react-mgallery
Version:
React component for mobile web gallery
350 lines (299 loc) • 11.3 kB
JSX
import React from 'react';
import './style.less';
// 全屏视窗
let _viewSize = { w: window.innerWidth, h: window.innerHeight },
_viewCenterPoint = { x: _viewSize.w/2, y: _viewSize.h/2 };
let _direction,// 'x', 'y', 0
_initialDistance,
_initialScale,
_initialScalerX,
_initialScalerY,
_initialCenterPoint,
_lastCenterPoint;
const DIRECTION_CHECK_OFFSET = 9;
// 计算两点间距
function _calculatePointsDistance(p1, p2) {
let x0 = Math.abs(p1.x - p2.x);
let y0 = Math.abs(p1.y - p2.y);
return Math.round(Math.sqrt(x0*x0 + y0*y0));
}
function _calculateCenterPoint(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p1.y) / 2
};
}
function _touchToPoint(touch) {
return { x: touch.pageX, y: touch.pageY };
}
function _limit(min, val, max){
return Math.max(min, Math.min(max, val));
}
// 计算图片缩放后的尺寸超出视窗边界
function _calculateBounds(destScale, item = _viewSize) {
let delta = {
x: (item.w * destScale - _viewSize.w) / 2,
y: (item.h * destScale - _viewSize.h) / 2
};
// 计算缩放后拖拽边界
let bounds = {
minX: delta.x < 0 ? 0 : -delta.x,
maxX: delta.x < 0 ? 0 : delta.x,
minY: delta.y < 0 ? 0 : -delta.y,
maxY: delta.y < 0 ? 0 : delta.y,
};
if(item.long) {
bounds.maxY = item.h * (destScale - 1) / 2;
bounds.minY = -((item.h - _viewSize.h) + item.h * (destScale - 1) / 2);
}
return bounds;
}
export default React.createClass({
getDefaultProps: function () {
return {
gap: 30,// 切换过程中相邻图片的间隙
actionDistance: 10,// 触发切换图片需要拖拽的距离
currentIndex: 0,
maxScale: 2,
minScale: 1,
onLoading() {},
onLoaded() {},
};
},
getInitialState() {
return {
currentIndex: this.props.currentIndex || 0,
lazyImgs: {},
imgData: {},
x0: 0,// x轴滑动偏移量
scale: 1,
scalerX: 0,
scalerY: 0,
transition: 0
};
},
init(touches) {
let p1 = _touchToPoint(touches[0]);
const {scale, scalerX, scalerY} = this.state;
if(touches.length > 1){
let p2 = _touchToPoint(touches[1]);
// 缓存两个触点间初始距离
_initialDistance = _calculatePointsDistance(p1, p2);
_initialCenterPoint = _calculateCenterPoint(p1, p2);
} else {
_initialCenterPoint = p1;
}
_direction = 0;
_lastCenterPoint = _viewCenterPoint;// tap聚焦效果
_initialScale = scale;
_initialScalerX = scalerX;
_initialScalerY = scalerY;
},
touchStart(e) {
e.preventDefault();
this.init(e.touches);
},
touchMove(e) {
e.preventDefault();
this.setState({ transition: 0 });
let { touches } = e;
let p1 = _lastCenterPoint = _touchToPoint(touches[0]);
if(touches.length > 1) {
// 缩放
let p2 = _touchToPoint(touches[1]);
let scale = _calculatePointsDistance(p1, p2) / _initialDistance;
let destScale = scale * _initialScale;
_lastCenterPoint = _calculateCenterPoint(p1, p2);
this.applyScale(destScale);
} else {
// 滑动
const { scale, currentIndex, imgData } = this.state;
const { gap, imgs } = this.props;
let scalerX = p1.x - _initialCenterPoint.x + _initialScalerX;
let scalerY = p1.y - _initialCenterPoint.y + _initialScalerY;
let item = imgData[currentIndex];
let { minX, minY, maxX, maxY } = _calculateBounds(scale, item);
let x0 = 0;
// 限制图片间间隙
minX -= gap;
maxX += gap;
if(scalerX > maxX) {
x0 = scalerX - maxX;
scalerX = maxX;
}
else if(scalerX < minX) {
x0 = scalerX - minX;
scalerX = minX;
}
// 第一张图限制右划切换
if(currentIndex == 0) x0 = Math.min(0, x0);
// 最后一张图限制左划切换
if(currentIndex == imgs.length - 1) x0 = Math.max(0, x0);
// 应用边界
scalerY = _limit(minY, scalerY, maxY);
let nextState = {x0, scalerX, scalerY};
// 水平切换发生的阈值
if(Math.abs(scalerX) < DIRECTION_CHECK_OFFSET) {
nextState.scalerX = 0;
}
// 未缩放
if(scale == 1 && item && item.long) {
// 判断滚动方向
if(!_direction) {
let deltaX = Math.abs(scalerX - _initialScalerX);
let deltaY = Math.abs(scalerY - _initialScalerY);
if(deltaY > DIRECTION_CHECK_OFFSET) _direction = 'y';
else if(deltaX > DIRECTION_CHECK_OFFSET) _direction = 'x';
}
if(_direction == 'x') {
// 发生水平滚动时,限制垂直移动
delete nextState.scalerY;
}
else if (_direction == 'y') {
// 发生垂直滚动时,限制水平移动
nextState.scalerX = nextState.x0 = 0;
}
}
this.setState(nextState);
}
},
touchEnd(e) {
const { scale, currentIndex, x0, imgData } = this.state;
const { maxScale, minScale, actionDistance, imgs } = this.props;
let { touches } = e;
if(touches.length) {
// 为剩下的触点重新初始化
this.init(touches);
} else {
// 触摸结束
let transition = 1;
let nextIndex = currentIndex;
// 判断切换图片
if(x0 < -actionDistance) nextIndex++;
else if(x0 > actionDistance) nextIndex--;
nextIndex = _limit(0, nextIndex, imgs.length - 1);
if(nextIndex != currentIndex) {
// 切换
this.setState({
transition,
currentIndex: nextIndex,
// 重置
scale: 1,
scalerX: 0,
scalerY: 0,
x0: 0,
});
this.preLoadImg();
} else {
// 应用缩放及滑动边界
let destScale = _limit(minScale, scale, maxScale);
let bounds = _calculateBounds(destScale, imgData[currentIndex]);
this.applyScale(destScale, bounds, transition);
}
}
},
applyScale(destScale, bounds, transition) {
const { imgData, currentIndex } = this.state;
let item = imgData[currentIndex];
let baseY = item.long ? item.h / 2 : _viewCenterPoint.y;
let baseX = _viewCenterPoint.x;
// 计算缩放补偿量
// 触摸中点移动距离 - 缩放导致的触摸中点偏移量
let scale = destScale / _initialScale;// 相对缩放率,以触摸开始为1
let scalerX = (_initialScalerX + _lastCenterPoint.x - _initialCenterPoint.x) * scale
- (_lastCenterPoint.x - baseX) * (scale - 1);
let scalerY = (_initialScalerY + _lastCenterPoint.y - _initialCenterPoint.y) * scale
- (_lastCenterPoint.y - baseY) * (scale - 1);
if(bounds) {
// 应用边界
scalerX = _limit(bounds.minX, scalerX, bounds.maxX);
scalerY = _limit(bounds.minY, scalerY, bounds.maxY);
}
// 计算缩放
this.setState({
scale: destScale,
scalerX,
scalerY,
x0: 0,
transition
});
},
preLoadImg(index) {
const { currentIndex, lazyImgs } = this.state;
index = index || currentIndex;
let _lazyImgs = {};
let nextIndex = Math.min(this.props.imgs.length - 1, index + 1);
let prevIndex = Math.max(0, index - 1);
_lazyImgs[index] = 1;
_lazyImgs[prevIndex] = 1;
_lazyImgs[nextIndex] = 1;
this.setState({ lazyImgs: Object.assign({}, lazyImgs, _lazyImgs)});
},
imgLoaded(e, i) {
const { imgData, currentIndex } = this.state;
if(imgData[i]) return;
if(i == currentIndex) this.props.onLoaded();
// 记录图片初始尺寸
let img = e.target;
let { width, height, naturalWidth, naturalHeight } = img;
let fitH = _viewSize.w / naturalWidth * naturalHeight;// 适应屏幕宽度后的高度
let long = fitH > _viewSize.h;
width = Math.min(naturalWidth, _viewSize.w);
height = width * naturalHeight / naturalWidth;
this.setState({
imgData: Object.assign({
[i] : {
w: width,
h: height,
long
}
}, imgData)
});
},
render(){
var { imgs } = this.props;
var {
x0, scale, scalerX, scalerY,
currentIndex, transition, lazyImgs, imgData
} = this.state;
var { touchStart, touchMove, touchEnd, imgLoaded } = this;
return (
<div className={'mgallery' + (transition ? ' transition' : '')} ref="holder"
onTouchStart={touchStart}
onTouchMove={touchMove}
onTouchEnd={touchEnd}>
<div className="mgallery-container" style={{
width: `${imgs.length * 100}%`,
WebkitTransform: `translateX(${x0 - currentIndex * _viewSize.w}px)`
}}>
{ imgs.map((img, i) => {
let item = imgData[i];
let scalerStyle = (i == currentIndex) && item ? {
WebkitTransform: `translate3d(${scalerX}px,${scalerY}px,0) scale(${scale})`
} : {};
let itemClass = 'mgallery-item ';
if(item && item.long) itemClass += 'long';
return (
<div key={i} className={itemClass}>
<div className="mgallery-scaler" style={scalerStyle}>
{ lazyImgs[i] ?
<img src={img} onLoad={(e) => imgLoaded(e, i)}/>
: null }
</div>
</div>
);
})}
</div>
<div className="mgallery-dots">
{ imgs.length > 1 ? imgs.map(function(img, i) {
return <i key={i} className={i == currentIndex ? 'on' : ''}/>;
}) : null }
</div>
</div>
);
},
componentWillMount() {
this.preLoadImg();
this.props.onLoading();
}
});