import React from 'react'; import { Motion, spring } from 'react-motion'; import throttle from 'lodash.throttle'; import Photo from './Photo'; import PhotoWrap from './components/PhotoWrap'; import isMobile from './utils/isMobile'; import getMultipleTouchPosition from './utils/getMultipleTouchPosition'; import getPositionOnMoveOrScale from './utils/getPositionOnMoveOrScale'; import slideToSuitableOffset from './utils/slideToSuitableOffset'; import { getClosedHorizontal, getClosedVertical } from './utils/getCloseEdge'; import { defaultAnimationConfig, minReachOffset, minScale, maxScale, scaleBuffer, } from './variables'; type ReachFunction = (pageX: number, pageY: number) => void; export interface IPhotoViewProps { // 图片地址 src: string; // 容器类名 wrapClassName?: string; // 图片类名 className?: string; // style style?: object; // 缩放,用于下拉关闭变小的效果 photoScale?: number; // 到达顶部滑动事件 onReachTopMove?: ReachFunction; // 到达右部滑动事件 onReachRightMove?: ReachFunction; // 到达底部滑动事件 onReachBottomMove?: ReachFunction; // 到达左部滑动事件 onReachLeftMove?: ReachFunction; // 触摸解除事件 onReachUp?: ReachFunction; onPhotoResize?: () => void; } const initialState = { // 图片 X 偏移量 x: 0, // 图片 y 偏移量 y: 0, // 图片缩放程度 scale: 1, // 图片处于触摸的状态 touched: false, // 触摸开始时 x 原始坐标 pageX: 0, // 触摸开始时 y 原始坐标 pageY: 0, // 触摸开始时图片 x 偏移量 lastX: 0, // 触摸开始时图片 y 偏移量 lastY: 0, // 触摸开始时时间 touchedTime: 0, // 多指触控间距 lastTouchLength: 0, // 动画类型 animation: defaultAnimationConfig, // 当前边缘触发状态,0: 未触发,1: x 轴,2: y 轴 reachState: 0, }; export default class PhotoView extends React.Component< IPhotoViewProps, typeof initialState > { static displayName = 'PhotoView'; readonly state = initialState; private photoRef; constructor(props) { super(props); this.handleMove = throttle(this.handleMove, 8); } componentDidMount() { if (isMobile) { window.addEventListener('touchmove', this.handleTouchMove, { passive: false }); window.addEventListener('touchend', this.handleTouchEnd); } else { window.addEventListener('mousemove', this.handleMouseMove); window.addEventListener('mouseup', this.handleMouseUp); } } componentWillUnmount() { if (isMobile) { window.removeEventListener('touchmove', this.handleTouchMove); window.removeEventListener('touchend', this.handleTouchEnd); } else { window.removeEventListener('mousemove', this.handleMouseMove); window.removeEventListener('mouseup', this.handleMouseUp); } } handleStart = (pageX: number, pageY: number, touchLength: number = 0) => { this.setState(prevState => ({ touched: true, pageX, pageY, lastX: prevState.x, lastY: prevState.y, lastTouchLength: touchLength, touchedTime: Date.now(), })); } handleMove = (newPageX: number, newPageY: number, touchLength: number = 0) => { if (this.state.touched) { const { width, naturalWidth } = this.photoRef.state; const { x, y, pageX, pageY, lastX, lastY, scale, lastTouchLength, reachState, } = this.state; let currentX = x; let currentY = y; // 边缘状态 let currentReachState = 0; if (touchLength === 0) { currentX = newPageX - pageX + lastX; currentY = newPageY - pageY + lastY; // 边缘触发检测 currentReachState = this.handleReachCallback( currentX, currentY, scale, newPageX, newPageY, reachState, ); } // 横向边缘触发禁用当前滑动 if (currentReachState === 1) { this.setState({ reachState: 1, }); } else { // 目标倍数 const endScale = scale + (touchLength - lastTouchLength) / 100 / 2 * scale; // 限制最大倍数和最小倍数 const toScale = Math.max( Math.min( endScale, Math.max(maxScale, naturalWidth / width) ), minScale - scaleBuffer, ); this.setState({ lastTouchLength: touchLength, reachState: currentReachState, ...getPositionOnMoveOrScale({ x: currentX, y: currentY, pageX: newPageX, pageY: newPageY, fromScale: scale, toScale, }), }); } } } handleDoubleClick = (e) => { const { pageX, pageY } = e; const { width, naturalWidth } = this.photoRef.state; this.setState(({ x, y, scale }) => { return { pageX, pageY, ...getPositionOnMoveOrScale({ x, y, pageX, pageY, fromScale: scale, // 若图片足够大,则放大适应的倍数 toScale: scale !== 1 ? 1 : Math.max(2, naturalWidth / width), }), animation: defaultAnimationConfig, }; }); } handleWheel = (e) => { e.preventDefault(); const { pageX, pageY, deltaY } = e; const { width, naturalWidth } = this.photoRef.state; this.setState(({ x, y, scale }) => { const endScale = scale - deltaY / 100 / 2; // 限制最大倍数和最小倍数 const toScale = Math.max( Math.min( endScale, Math.max(maxScale, naturalWidth / width) ), minScale, ); return { pageX, pageY, ...getPositionOnMoveOrScale({ x, y, pageX, pageY, fromScale: scale, toScale, }), animation: defaultAnimationConfig, }; }); } handleTouchStart = e => { if (e.touches.length >= 2) { const { pageX, pageY, touchLength } = getMultipleTouchPosition(e); this.handleStart(pageX, pageY, touchLength); } else { const { pageX, pageY } = e.touches[0]; this.handleStart(pageX, pageY); } } handleMouseDown = e => { e.preventDefault(); this.handleStart(e.pageX, e.pageY); } handleTouchMove = e => { e.preventDefault(); if (e.touches.length >= 2) { const { pageX, pageY, touchLength } = getMultipleTouchPosition(e); this.handleMove(pageX, pageY, touchLength); } else { const { pageX, pageY } = e.touches[0]; this.handleMove(pageX, pageY); } } handleMouseMove = e => { e.preventDefault(); this.handleMove(e.pageX, e.pageY); } handleUp = (newPageX: number, newPageY: number) => { if (this.state.touched) { const { onReachUp } = this.props; const { width, height } = this.photoRef.state; this.setState(({ x, y, lastX, lastY, scale, touchedTime, pageX, pageY, }) => { if (onReachUp) { onReachUp(newPageX, newPageY); } const hasMove = pageX !== newPageX || pageY !== newPageY; // 缩放弹性效果 const toScale = Math.max( Math.min( scale, maxScale, ), minScale, ); return { touched: false, scale: toScale, reachState: 0, // 重置触发状态 ...hasMove ? slideToSuitableOffset({ x, y, lastX, lastY, width, height, scale, touchedTime, }) : { x, y, animation: defaultAnimationConfig, }, }; }); } } handleTouchEnd = (e) => { const { pageX, pageY } = e.changedTouches[0]; this.handleUp(pageX, pageY); } handleMouseUp = (e) => { const { pageX, pageY } = e; this.handleUp(pageX, pageY); } handleResize = () => { this.setState(initialState); const { onPhotoResize } = this.props; if (onPhotoResize) { onPhotoResize(); } } handleReachCallback = ( x: number, y: number, scale: number, newPageX: number, newPageY: number, reachState: number, ): number => { const { width, height } = this.photoRef.state; const horizontalType = getClosedHorizontal(x, scale, width); const verticalType = getClosedVertical(y, scale, height); const { onReachTopMove, onReachRightMove, onReachBottomMove, onReachLeftMove, } = this.props; // 触碰到边缘 if ( onReachLeftMove && (horizontalType && x > minReachOffset && reachState === 0 || reachState === 1) ) { onReachLeftMove(newPageX, newPageY); return 1; } else if ( onReachRightMove && (horizontalType && x < -minReachOffset && reachState === 0 || reachState === 1) ) { onReachRightMove(newPageX, newPageY); return 1; } else if ( onReachTopMove && (verticalType && y > minReachOffset && reachState === 0 || reachState === 2) ) { onReachTopMove(newPageX, newPageY); return 2; } else if ( onReachBottomMove && (verticalType && y < -minReachOffset && reachState === 0 || reachState === 2) ) { onReachBottomMove(newPageX, newPageY); return 2; } return 0; } handlePhotoRef = (ref) => { this.photoRef = ref; } render() { const { src, wrapClassName, className, style, photoScale = 1 } = this.props; const { x, y, scale, touched, animation } = this.state; return ( {({ currX, currY, currScale }) => { const transform = `translate3d(${currX}px, ${currY}px, 0) scale(${currScale * photoScale})`; return ( ); }} ); } }