123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- 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<IPhotoSliderProps, PhotoSliderState> {
- 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 (
- <VisibleAnimationHandle visible={visible} currentImage={currentImage}>
- {({ 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 (
- <SlideWrap className={className}>
- <div
- className={classNames('PhotoView-PhotoSlider__Backdrop', maskClassName, {
- 'PhotoView-PhotoSlider__fadeIn': showAnimateType === ShowAnimateEnum.In,
- 'PhotoView-PhotoSlider__fadeOut': showAnimateType === ShowAnimateEnum.Out,
- })}
- style={{
- background: `rgba(0, 0, 0, ${currentOpacity})`,
- }}
- onAnimationEnd={onShowAnimateEnd}
- />
- {bannerVisible && (
- <div className="PhotoView-PhotoSlider__BannerWrap" style={overlayStyle}>
- <div className="PhotoView-PhotoSlider__Counter">
- {photoIndex + 1} / {imageLength}
- </div>
- <div className="PhotoView-PhotoSlider__BannerRight">
- <CloseSVG
- className="PhotoView-PhotoSlider__Close"
- onTouchEnd={isTouchDevice ? this.handleClose : undefined}
- onClick={isTouchDevice ? undefined : this.handleClose}
- />
- </div>
- </div>
- )}
- {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 (
- <PhotoView
- key={item.key || realIndex}
- src={item.src}
- onReachMove={this.handleReachMove}
- onReachUp={this.handleReachUp}
- onPhotoTap={this.handlePhotoTap}
- onMaskTap={this.handlePhotoMaskTap}
- viewClassName={viewClassName}
- className={imageClassName}
- style={{
- left: `${(innerWidth + horizontalOffset) * realIndex}px`,
- WebkitTransform: transform,
- transform,
- transition:
- touched || !shouldTransition
- ? undefined
- : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
- }}
- loadingElement={loadingElement}
- brokenElement={brokenElement}
- onPhotoResize={this.handleResize}
- isActive={photoIndex === realIndex}
- showAnimateType={showAnimateType}
- originRect={originRect}
- />
- );
- })}
- {Boolean(introVisible && overlayIntro) && (
- <div className="PhotoView-PhotoSlider__FooterWrap" style={overlayStyle}>
- {overlayIntro}
- </div>
- )}
- {overlayRender &&
- overlayRender({
- images,
- index: photoIndex,
- visible,
- onClose: this.handleClose,
- onIndexChange: this.handleIndexChange,
- overlayVisible: currentOverlayVisible,
- })}
- </SlideWrap>
- );
- }
- return null;
- }}
- </VisibleAnimationHandle>
- );
- }
- }
|