|
|
@@ -0,0 +1,530 @@
|
|
|
+import React, {PureComponent} from 'react';
|
|
|
+import PropTypes from 'prop-types';
|
|
|
+
|
|
|
+import raf from 'raf';
|
|
|
+import tween from './tween.js';
|
|
|
+import Loading from './Loading';
|
|
|
+import {isObjNull} from "../../utils/common";
|
|
|
+
|
|
|
+/**
|
|
|
+ *
|
|
|
+ * @param {number} value
|
|
|
+ * @param {number} min
|
|
|
+ * @param {number} max
|
|
|
+ */
|
|
|
+function setScope(value, min, max) {
|
|
|
+ if (value < min) {
|
|
|
+ return min;
|
|
|
+ }
|
|
|
+ if (value > max) {
|
|
|
+ return max;
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+function getDistanceBetweenTouches(e) {
|
|
|
+ if (e.touches.length < 2) return 1;
|
|
|
+ const x1 = e.touches[0].clientX;
|
|
|
+ const y1 = e.touches[0].clientY;
|
|
|
+ const x2 = e.touches[1].clientX;
|
|
|
+ const y2 = e.touches[1].clientY;
|
|
|
+ const distance = Math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2));
|
|
|
+ return distance;
|
|
|
+}
|
|
|
+
|
|
|
+// const msPerFrame = 1000 / 60;
|
|
|
+const maxAnimateTime = 1000;
|
|
|
+const minTapMoveValue = 5;
|
|
|
+const maxTapTimeValue = 300;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 图片默认展示模式:宽度等于屏幕宽度,高度等比缩放;水平居中,垂直居中或者居顶(当高度大于屏幕高度时)
|
|
|
+ * 图片实际尺寸: actualWith, actualHeight
|
|
|
+ * 图片初始尺寸: originWidth, originHeight
|
|
|
+ * 坐标位置:left, top
|
|
|
+ * 放大倍数:zoom
|
|
|
+ * 最大放大倍数:maxZoomNum
|
|
|
+ * 坐标关系:-(maxZoomNum - 1) * originWidth / 2 < left < 0
|
|
|
+ * -(maxZoomNum - 1) * originHeight / 2 < top < 0
|
|
|
+ * 尺寸关系:width = zoom * originWidth
|
|
|
+ * heigth = zoom * originHeight
|
|
|
+ *
|
|
|
+ * 放大点位置关系:
|
|
|
+ * 初始点位置:oldPointLeft, oldPointTop
|
|
|
+ * 放大后位置:newPointLeft, newPointTop
|
|
|
+ * 对应关系: newPointLeft = zoom * oldPointLeft
|
|
|
+ * newPointTop = zoom * oldPointTop
|
|
|
+ *
|
|
|
+ * 坐标位置:-1*left = -1*startLeft + (newPointLeft - oldPointLeft) =-1*startLeft (zoom - 1) * oldPointLeft
|
|
|
+ * -1*top = -1*startTop + (newPointTop - oldPointTop) =-1*startLeft (zoom - 1) * oldPointTop
|
|
|
+ * =>
|
|
|
+ * left = startLeft + (1 - zoom) * oldPointLeft
|
|
|
+ * top = startTop + (1 - zoom) * oldPointTop
|
|
|
+ */
|
|
|
+
|
|
|
+class ImageContainer extends PureComponent {
|
|
|
+ static propTypes = {
|
|
|
+ maxZoomNum: PropTypes.number.isRequired,
|
|
|
+ handleStart: PropTypes.func.isRequired,
|
|
|
+ handleMove: PropTypes.func.isRequired,
|
|
|
+ handleEnd: PropTypes.func.isRequired,
|
|
|
+ }
|
|
|
+
|
|
|
+ static contextTypes = {
|
|
|
+ onClose: PropTypes.func,
|
|
|
+ };
|
|
|
+
|
|
|
+ constructor() {
|
|
|
+ super();
|
|
|
+ this.actualHeight = 0; // 图片实际高度
|
|
|
+ this.actualWith = 0; // 图片实际宽度
|
|
|
+
|
|
|
+ this.originHeight = 0; // 图片默认展示模式下高度
|
|
|
+ this.originWidth = 0; // 图片默认展示模式下宽度
|
|
|
+ this.originScale = 1; // 图片初始缩放比例
|
|
|
+
|
|
|
+ this.startLeft = 0; // 开始触摸操作时的 left 值
|
|
|
+ this.startTop = 0; // 开始触摸操作时的 top 值
|
|
|
+ this.startScale = 1; // 开始缩放操作时的 scale 值
|
|
|
+
|
|
|
+ this.onTouchStartTime = 0; // 单指触摸开始时间
|
|
|
+
|
|
|
+ this.isTwoFingerMode = false; // 是否为双指模式
|
|
|
+ this.oldPointLeft = 0;// 计算手指中间点在图片上的位置(坐标值)
|
|
|
+ this.oldPointTop = 0;// 计算手指中间点在图片上的位置(坐标值)
|
|
|
+ this._touchZoomDistanceStart = 0; // 用于记录双指距离
|
|
|
+ this.haveCallMoveFn = false;
|
|
|
+
|
|
|
+
|
|
|
+ this.diffX = 0;// 记录最后 move 事件 移动距离
|
|
|
+ this.diffY = 0;// 记录最后 move 事件 移动距离
|
|
|
+
|
|
|
+ this.animationID = 0;
|
|
|
+ this.animateStartTime = 0;
|
|
|
+ this.animateStartValue = {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ };
|
|
|
+ this.animateFinalValue = {
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ state = {
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ scale: 1,
|
|
|
+ left: 0,
|
|
|
+ top: 0,
|
|
|
+ isLoaded: false,
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillMount() {
|
|
|
+ this.loadImg(this.props.src);
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillUnmount() {
|
|
|
+ this.unloadImg();
|
|
|
+ if (this.animationID) {
|
|
|
+ raf.cancel(this.animationID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ onLoad = () => {
|
|
|
+ if (isObjNull(this.img)) {
|
|
|
+ this.img = new Image();
|
|
|
+ }
|
|
|
+ this.actualWith = this.img.width;
|
|
|
+ this.actualHeight = this.img.height;
|
|
|
+
|
|
|
+ const {
|
|
|
+ screenHeight,
|
|
|
+ screenWidth,
|
|
|
+ } = this.props;
|
|
|
+
|
|
|
+ const left = 0;
|
|
|
+ let top = 0;
|
|
|
+
|
|
|
+ this.originWidth = screenWidth;
|
|
|
+ this.originHeight = (this.actualHeight / this.actualWith) * screenWidth;
|
|
|
+ this.originScale = 1;
|
|
|
+
|
|
|
+ if (this.actualHeight / this.actualWith < screenHeight / screenWidth) {
|
|
|
+ top = parseInt((screenHeight - this.originHeight) / 2, 10);
|
|
|
+ }
|
|
|
+ this.originTop = top;
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ width: this.originWidth,
|
|
|
+ height: this.originHeight,
|
|
|
+ scale: 1,
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ isLoaded: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ onError = () => {
|
|
|
+ this.setState({
|
|
|
+ isLoaded: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ loadImg = (url) => {
|
|
|
+ this.img = new Image();
|
|
|
+ this.img.src = url;
|
|
|
+ this.img.onload = this.onLoad;
|
|
|
+ this.img.onerror = this.onError;
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ isLoaded: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ unloadImg = () => {
|
|
|
+ delete this.img.onerror;
|
|
|
+ delete this.img.onload;
|
|
|
+ delete this.img.src;
|
|
|
+ delete this.img;
|
|
|
+ }
|
|
|
+
|
|
|
+ handleTouchStart = (event) => {
|
|
|
+ event.preventDefault();
|
|
|
+ if (this.animationID) {
|
|
|
+ raf.cancel(this.animationID);
|
|
|
+ }
|
|
|
+ switch (event.touches.length) {
|
|
|
+ case 1: {
|
|
|
+ const targetEvent = event.touches[0];
|
|
|
+ this.startX = targetEvent.clientX;
|
|
|
+ this.startY = targetEvent.clientY;
|
|
|
+ this.diffX = 0;
|
|
|
+ this.diffY = 0;
|
|
|
+
|
|
|
+ this.startLeft = this.state.left;
|
|
|
+ this.startTop = this.state.top;
|
|
|
+
|
|
|
+ this.onTouchStartTime = (new Date()).getTime();
|
|
|
+ this.haveCallMoveFn = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 2: { // 两个手指
|
|
|
+ // 设置手双指模式
|
|
|
+ this.isTwoFingerMode = true;
|
|
|
+
|
|
|
+ // 计算两个手指中间点屏幕上的坐标
|
|
|
+ const middlePointClientLeft = Math.abs(Math.round((event.touches[0].clientX + event.touches[1].clientX) / 2));
|
|
|
+ const middlePointClientTop = Math.abs(Math.round((event.touches[0].clientY + event.touches[1].clientY) / 2));
|
|
|
+
|
|
|
+ // 保存图片初始位置和尺寸
|
|
|
+ this.startLeft = this.state.left;
|
|
|
+ this.startTop = this.state.top;
|
|
|
+ this.startScale = this.state.scale;
|
|
|
+
|
|
|
+ // 计算手指中间点在图片上的位置(坐标值)
|
|
|
+ this.oldPointLeft = middlePointClientLeft - this.startLeft;
|
|
|
+ this.oldPointTop = middlePointClientTop - this.startTop;
|
|
|
+
|
|
|
+ this._touchZoomDistanceStart = getDistanceBetweenTouches(event);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleTouchMove = (event) => {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ switch (event.touches.length) {
|
|
|
+ case 1: {
|
|
|
+ const targetEvent = event.touches[0];
|
|
|
+ const diffX = targetEvent.clientX - this.startX;
|
|
|
+ const diffY = targetEvent.clientY - this.startY;
|
|
|
+
|
|
|
+ this.diffX = diffX;
|
|
|
+ this.diffY = diffY;
|
|
|
+ // 判断是否为点击
|
|
|
+ if (Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const {scale, left} = this.state;
|
|
|
+ const width = scale * this.originWidth;
|
|
|
+ if (Math.abs(diffX) > Math.abs(diffY)) {
|
|
|
+ // 水平移动
|
|
|
+ if (this.state.scale === this.originScale && Math.abs(diffX) > minTapMoveValue) {
|
|
|
+ this.haveCallMoveFn = true;
|
|
|
+ this.callHandleMove(diffX);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (diffX < 0 && this.startLeft <= this.originWidth - width) {
|
|
|
+ this.haveCallMoveFn = true;
|
|
|
+ this.callHandleMove(diffX);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (diffX > 0 && this.startLeft >= 0) {
|
|
|
+ this.haveCallMoveFn = true;
|
|
|
+ this.callHandleMove(diffX);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const {screenHeight} = this.props;
|
|
|
+ const height = scale * this.originHeight;
|
|
|
+ let newTop = (screenHeight - height) / 2;
|
|
|
+ const newLeft = this.startLeft + diffX;
|
|
|
+
|
|
|
+ if (height > screenHeight || this.state.scale === this.originScale) {
|
|
|
+ newTop = this.startTop + diffY;
|
|
|
+ }
|
|
|
+ this.setState({
|
|
|
+ left: newLeft,
|
|
|
+ top: newTop,
|
|
|
+ });
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 2: { // 两个手指
|
|
|
+ this._touchZoomDistanceEnd = getDistanceBetweenTouches(event);
|
|
|
+
|
|
|
+ const zoom = Math.sqrt(this._touchZoomDistanceEnd / this._touchZoomDistanceStart);
|
|
|
+ const scale = zoom * this.startScale;
|
|
|
+
|
|
|
+ this.setState(() => {
|
|
|
+ const left = this.startLeft + ((1 - zoom) * this.oldPointLeft);
|
|
|
+ const top = this.startTop + ((1 - zoom) * this.oldPointTop);
|
|
|
+
|
|
|
+ return {
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ scale,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ handleTouchEnd = (event) => {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ if (this.isTwoFingerMode) { // 双指操作结束
|
|
|
+ const touchLen = event.touches.length;
|
|
|
+ this.isTwoFingerMode = false;
|
|
|
+
|
|
|
+ if (touchLen === 1) {
|
|
|
+ const targetEvent = event.touches[0];
|
|
|
+ this.startX = targetEvent.clientX;
|
|
|
+ this.startY = targetEvent.clientY;
|
|
|
+ this.diffX = 0;
|
|
|
+ this.diffY = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.setState((prevState, props) => {
|
|
|
+ const scale = setScope(prevState.scale, 1, props.maxZoomNum);
|
|
|
+ const width = scale * this.originWidth;
|
|
|
+ const height = scale * this.originHeight;
|
|
|
+ const zoom = scale / this.startScale;
|
|
|
+ const left = setScope(this.startLeft + ((1 - zoom) * this.oldPointLeft), this.originWidth - width, 0);
|
|
|
+
|
|
|
+ let top;
|
|
|
+ if (height > props.screenHeight) {
|
|
|
+ top = setScope(this.startTop + ((1 - zoom) * this.oldPointTop), props.screenHeight - height, 0);
|
|
|
+ } else {
|
|
|
+ top = (props.screenHeight - height) / 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (touchLen === 1) {
|
|
|
+ this.startLeft = left;
|
|
|
+ this.startTop = top;
|
|
|
+ this.startScale = scale;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ scale,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } else { // 单指结束(ontouchend)
|
|
|
+ const diffTime = (new Date()).getTime() - this.onTouchStartTime;
|
|
|
+ const {diffX, diffY} = this;
|
|
|
+
|
|
|
+ // 判断为点击则关闭图片浏览组件
|
|
|
+ if (diffTime < maxTapTimeValue && Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
|
|
|
+ this.context.onClose();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 水平移动
|
|
|
+ if (this.haveCallMoveFn) {
|
|
|
+ const isChangeImage = this.callHandleEnd(diffY < 30);
|
|
|
+ if (isChangeImage) { // 如果切换图片则重置当前图片状态
|
|
|
+ setTimeout(() => {
|
|
|
+ this.setState({
|
|
|
+ scale: this.originScale,
|
|
|
+ left: 0,
|
|
|
+ top: this.originTop,
|
|
|
+ });
|
|
|
+ }, maxAnimateTime / 3);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // TODO 下拉移动距离超过屏幕高度的 1/3 则关闭
|
|
|
+ // console.info(Math.abs(diffY) > (this.props.screenHeight / 2), this.startTop, this.originTop);
|
|
|
+ // if (Math.abs(diffX) < Math.abs(diffY) && Math.abs(diffY) > (this.props.screenHeight / 3) && this.startTop === this.originTop) {
|
|
|
+ // this.context.onClose();
|
|
|
+ // return;
|
|
|
+ // }
|
|
|
+
|
|
|
+ let x;
|
|
|
+ let y;
|
|
|
+ const {scale} = this.state;
|
|
|
+ // const width = scale * this.originWidth;
|
|
|
+ const height = scale * this.originHeight;
|
|
|
+
|
|
|
+ // 使用相同速度算法
|
|
|
+ x = ((diffX * maxAnimateTime) / diffTime) + this.startLeft;
|
|
|
+ y = ((diffY * maxAnimateTime) / diffTime) + this.startTop;
|
|
|
+
|
|
|
+ if (this.state.scale === this.originScale) {
|
|
|
+ x = 0;
|
|
|
+ if (height > this.props.screenHeight) {
|
|
|
+ y = setScope(y, this.props.screenHeight - height, 0);
|
|
|
+ } else {
|
|
|
+ y = this.originTop;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // x = setScope(x, this.originWidth - width, 0);
|
|
|
+
|
|
|
+ // if (height > this.props.screenHeight) {
|
|
|
+ // y = setScope(y, this.props.screenHeight - height, 0);
|
|
|
+ // } else {
|
|
|
+ // y = this.state.top;
|
|
|
+ // }
|
|
|
+
|
|
|
+ this.animateStartValue = {
|
|
|
+ x: this.state.left,
|
|
|
+ y: this.state.top,
|
|
|
+ };
|
|
|
+ this.animateFinalValue = {
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ };
|
|
|
+ this.animateStartTime = Date.now();
|
|
|
+ this.startAnimate();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ startAnimate = () => {
|
|
|
+ this.animationID = raf(() => {
|
|
|
+ // calculate current time
|
|
|
+ const curTime = Date.now() - this.animateStartTime;
|
|
|
+ let left;
|
|
|
+ let top;
|
|
|
+
|
|
|
+ // animate complete
|
|
|
+ if (curTime > maxAnimateTime) {
|
|
|
+ this.setState((prevState, props) => {
|
|
|
+ const width = prevState.scale * this.originWidth;
|
|
|
+ const height = prevState.scale * this.originHeight;
|
|
|
+ left = setScope(prevState.left, this.originWidth - width, 0);
|
|
|
+
|
|
|
+ if (height > props.screenHeight) {
|
|
|
+ top = setScope(prevState.top, props.screenHeight - height, 0);
|
|
|
+ } else {
|
|
|
+ top = (props.screenHeight - height) / 2;
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ left = tween.easeOutQuart(curTime, this.animateStartValue.x, this.animateFinalValue.x, maxAnimateTime);
|
|
|
+ top = tween.easeOutQuart(curTime, this.animateStartValue.y, this.animateFinalValue.y, maxAnimateTime);
|
|
|
+
|
|
|
+ this.setState({
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ });
|
|
|
+ this.startAnimate();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ callHandleMove = (diffX) => {
|
|
|
+ if (!this.isCalledHandleStart) {
|
|
|
+ this.isCalledHandleStart = true;
|
|
|
+ if (this.props.handleStart) {
|
|
|
+ this.props.handleStart();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.props.handleMove(diffX);
|
|
|
+ }
|
|
|
+
|
|
|
+ callHandleEnd = (isAllowChange) => {
|
|
|
+ if (this.isCalledHandleStart) {
|
|
|
+ this.isCalledHandleStart = false;
|
|
|
+ if (this.props.handleEnd) {
|
|
|
+ return this.props.handleEnd(isAllowChange);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ render() {
|
|
|
+ const {
|
|
|
+ screenWidth,
|
|
|
+ screenHeight,
|
|
|
+ src,
|
|
|
+ left: divLeft,
|
|
|
+ } = this.props;
|
|
|
+
|
|
|
+ const {
|
|
|
+ isLoaded,
|
|
|
+ left,
|
|
|
+ top,
|
|
|
+ scale,
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ } = this.state;
|
|
|
+
|
|
|
+ const ImageStyle = {
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ };
|
|
|
+
|
|
|
+ const translate = `translate3d(${left}px, ${top}px, 0) scale(${scale})`;
|
|
|
+ ImageStyle.WebkitTransform = translate;
|
|
|
+ ImageStyle.transform = translate;
|
|
|
+
|
|
|
+ const defaultStyle = {
|
|
|
+ left: divLeft,
|
|
|
+ width: screenWidth,
|
|
|
+ height: screenHeight,
|
|
|
+ };
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className="viewer-image-container"
|
|
|
+ onTouchStart={this.handleTouchStart}
|
|
|
+ onTouchMove={this.handleTouchMove}
|
|
|
+ onTouchEnd={this.handleTouchEnd}
|
|
|
+ style={defaultStyle}
|
|
|
+ >
|
|
|
+ {
|
|
|
+ isLoaded ? <img src={src} style={ImageStyle} alt=""/> : <Loading/>
|
|
|
+ }
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export default ImageContainer;
|