import React from 'react'; import classNames from 'classnames'; import debounce from 'lodash.debounce'; import PhotoView from './PhotoView'; import SlideWrap from './components/SlideWrap'; import VisibleAnimationHandle from './components/VisibleAnimationHandle'; import CloseSVG from './components/CloseSVG'; import isTouchDevice from './utils/isTouchDevice'; import { dataType, IPhotoProviderBase, ReachTypeEnum, ShowAnimateEnum } from './types'; import { defaultOpacity, horizontalOffset, maxMoveOffset } from './variables'; import './PhotoSlider.less'; export interface IPhotoSliderProps extends IPhotoProviderBase { // 图片列表 images: dataType[]; // 图片当前索引 index?: number; // 可见 visible: boolean; // 关闭事件 onClose: (evt?: React.MouseEvent | React.TouchEvent) => void; // 索引改变回调 onIndexChange?: Function; } type PhotoSliderState = { // 偏移量 translateX: number; // 图片当前的 index photoIndex: number; // 图片处于触摸的状态 touched: boolean; // 该状态是否需要 transition shouldTransition: boolean; // Reach 开始时 x 坐标 lastClientX: number | undefined; // Reach 开始时 y 坐标 lastClientY: number | undefined; // 背景透明度 backdropOpacity: number; // 上次关闭的背景透明度 lastBackdropOpacity: number; // 覆盖物可见度 overlayVisible: boolean; // 可下拉关闭 canPullClose: boolean; }; export default class PhotoSlider extends React.Component { static displayName = 'PhotoSlider'; static defaultProps = { maskClosable: true, photoClosable: false, bannerVisible: true, introVisible: true, }; static getDerivedStateFromProps(nextProps, prevState) { if (nextProps.index !== undefined && nextProps.index !== prevState.photoIndex) { return { photoIndex: nextProps.index, translateX: -(window.innerWidth + horizontalOffset) * nextProps.index, }; } return null; } constructor(props) { super(props); this.state = { translateX: 0, photoIndex: 0, touched: false, shouldTransition: true, lastClientX: undefined, lastClientY: undefined, backdropOpacity: defaultOpacity, lastBackdropOpacity: defaultOpacity, overlayVisible: true, canPullClose: true, }; this.handleResize = debounce(this.handleResize, 32); } componentDidMount() { const { index = 0 } = this.props; this.setState({ translateX: index * -(window.innerWidth + horizontalOffset), photoIndex: index, }); window.addEventListener('keydown', this.handleKeyDown); } componentWillUnmount() { window.removeEventListener('keydown', this.handleKeyDown); } handleClose = () => { const { onClose } = this.props; const { backdropOpacity } = this.state; onClose(); this.setState({ overlayVisible: true, // 记录当前关闭时的透明度 lastBackdropOpacity: backdropOpacity, }); }; handlePhotoTap = () => { const { photoClosable } = this.props; if (photoClosable) { this.handleClose(); } else { this.setState(prevState => ({ overlayVisible: !prevState.overlayVisible, })); } }; handlePhotoMaskTap = () => { const { maskClosable } = this.props; if (maskClosable) { this.handleClose(); } }; handleResize = () => { const { innerWidth } = window; this.setState(({ photoIndex }) => { return { translateX: -(innerWidth + horizontalOffset) * photoIndex, lastClientX: undefined, lastClientY: undefined, }; }); }; handleKeyDown = (evt: KeyboardEvent) => { const { visible } = this.props; if (visible) { switch (evt.key) { case 'ArrowLeft': this.handlePrevious(false); break; case 'ArrowRight': this.handleNext(false); break; case 'Escape': this.handleClose(); break; } } }; handleReachVerticalMove = (clientY, scale) => { this.setState(({ lastClientY, backdropOpacity }) => { if (lastClientY === undefined) { return { touched: true, lastClientY: clientY, backdropOpacity, canPullClose: true, }; } const offsetClientY = Math.abs(clientY - lastClientY); const opacity = Math.max(Math.min(defaultOpacity, defaultOpacity - offsetClientY / 100 / 2), 0); return { touched: true, lastClientY, backdropOpacity: scale === 1 ? opacity : defaultOpacity, canPullClose: scale === 1, }; }); }; handleReachHorizontalMove = clientX => { const { innerWidth } = window; const { images } = this.props; this.setState(({ lastClientX, translateX, photoIndex }) => { if (lastClientX === undefined) { return { touched: true, lastClientX: clientX, translateX, shouldTransition: true, }; } const originOffsetClientX = clientX - lastClientX; let offsetClientX = originOffsetClientX; // 第一张和最后一张超出距离减半 if ( (photoIndex === 0 && originOffsetClientX > 0) || (photoIndex === images.length - 1 && originOffsetClientX < 0) ) { offsetClientX = originOffsetClientX / 2; } return { touched: true, lastClientX: lastClientX, translateX: -(innerWidth + horizontalOffset) * photoIndex + offsetClientX, shouldTransition: true, }; }); }; handleIndexChange = (photoIndex: number, shouldTransition: boolean = true) => { const singlePageWidth = window.innerWidth + horizontalOffset; const translateX = -singlePageWidth * photoIndex; this.setState({ touched: false, lastClientX: undefined, lastClientY: undefined, translateX, photoIndex, shouldTransition, }); const { onIndexChange } = this.props; if (onIndexChange) { onIndexChange(photoIndex); } }; handlePrevious = (shouldTransition?: boolean) => { const { photoIndex } = this.state; if (photoIndex > 0) { this.handleIndexChange(photoIndex - 1, shouldTransition); } }; handleNext = (shouldTransition?: boolean) => { const { images } = this.props; const { photoIndex } = this.state; if (photoIndex < images.length - 1) { this.handleIndexChange(photoIndex + 1, shouldTransition); } }; handleReachMove = (reachState: ReachTypeEnum, clientX: number, clientY: number, scale?: number) => { if (reachState === ReachTypeEnum.XReach) { this.handleReachHorizontalMove(clientX); } else if (reachState === ReachTypeEnum.YReach) { this.handleReachVerticalMove(clientY, scale); } }; handleReachUp = (clientX: number, clientY: number) => { const { images } = this.props; const { lastClientX = clientX, lastClientY = clientY, photoIndex, overlayVisible, canPullClose } = this.state; const offsetClientX = clientX - lastClientX; const offsetClientY = clientY - lastClientY; let willClose = false; // 下一张 if (offsetClientX < -maxMoveOffset && photoIndex < images.length - 1) { this.handleIndexChange(photoIndex + 1); return; } // 上一张 if (offsetClientX > maxMoveOffset && photoIndex > 0) { this.handleIndexChange(photoIndex - 1); return; } const singlePageWidth = window.innerWidth + horizontalOffset; // 当前偏移 let currentTranslateX = -singlePageWidth * photoIndex; let currentPhotoIndex = photoIndex; if (Math.abs(offsetClientY) > window.innerHeight * 0.14 && canPullClose) { willClose = true; this.handleClose(); } this.setState({ touched: false, translateX: currentTranslateX, photoIndex: currentPhotoIndex, lastClientX: undefined, lastClientY: undefined, backdropOpacity: defaultOpacity, overlayVisible: willClose ? true : overlayVisible, }); }; render() { const { images, visible, className, maskClassName, viewClassName, imageClassName, bannerVisible, introVisible, overlayRender, loadingElement, brokenElement, } = this.props; const { translateX, touched, photoIndex, backdropOpacity, lastBackdropOpacity, overlayVisible, shouldTransition, } = this.state; const imageLength = images.length; const currentImage = images.length ? images[photoIndex] : undefined; const transform = `translate3d(${translateX}px, 0px, 0)`; // Overlay const overlayIntro = currentImage && currentImage.intro; return ( {({ photoVisible, showAnimateType, originRect, onShowAnimateEnd }) => { if (photoVisible) { const { innerWidth } = window; const currentOverlayVisible = overlayVisible && showAnimateType === ShowAnimateEnum.None; const overlayStyle = { opacity: +currentOverlayVisible, }; // 关闭过程中使用下拉保存的透明度 const currentOpacity = visible ? backdropOpacity : lastBackdropOpacity; return (
{bannerVisible && (
{photoIndex + 1} / {imageLength}
)} {images .slice( // 加载相邻三张 Math.max(photoIndex - 1, 0), Math.min(photoIndex + 2, imageLength + 1), ) .map((item: dataType, index) => { // 截取之前的索引位置 const realIndex = photoIndex === 0 ? photoIndex + index : photoIndex - 1 + index; return ( ); })} {Boolean(introVisible && overlayIntro) && (
{overlayIntro}
)} {overlayRender && overlayRender({ images, index: photoIndex, visible, onClose: this.handleClose, onIndexChange: this.handleIndexChange, overlayVisible: currentOverlayVisible, })} ); } return null; }} ); } }