123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- import React from 'react';
- import classNames from 'classnames';
- import PhotoView from './PhotoView';
- import SlideWrap from './components/SlideWrap';
- import CloseSVG from './components/CloseSVG';
- import isMobile from './utils/isMobile';
- import './PhotoSlider.less';
- import { dataType, IPhotoProviderBase } from './types';
- import { defaultOpacity, horizontalOffset, maxMoveOffset } from './variables';
- 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;
- // Reach 开始时 x 坐标
- lastClientX: number | undefined;
- // Reach 开始时 y 坐标
- lastClientY: number | undefined;
- // 背景透明度
- backdropOpacity: number;
- // 缩放度
- photoScale: number;
- // 覆盖物可见度
- overlayVisible: 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,
- lastClientX: undefined,
- lastClientY: undefined,
- backdropOpacity: defaultOpacity,
- photoScale: 1,
- overlayVisible: true,
- };
- }
- componentDidMount() {
- const { index = 0 } = this.props;
- this.setState({
- translateX: index * -(window.innerWidth + horizontalOffset),
- photoIndex: index,
- });
- window.addEventListener('resize', this.handleResize);
- }
- componentWillUnmount() {
- window.removeEventListener('resize', this.handleResize);
- }
- handleClose = () => {
- this.props.onClose();
- this.setState({
- overlayVisible: true,
- });
- };
- handlePhotoTap = () => {
- if (this.props.photoClosable) {
- this.handleClose();
- } else {
- this.setState(prevState => ({
- overlayVisible: !prevState.overlayVisible,
- }));
- }
- };
- handlePhotoMaskTap = () => {
- if (this.props.maskClosable) {
- this.handleClose();
- }
- };
- handleResize = () => {
- const { innerWidth } = window;
- this.setState(({ photoIndex }) => {
- return {
- translateX: -(innerWidth + horizontalOffset) * photoIndex,
- lastClientX: undefined,
- lastClientY: undefined,
- };
- });
- };
- handleReachVerticalMove = (_, clientY) => {
- this.setState(({ lastClientY, backdropOpacity }) => {
- if (lastClientY === undefined) {
- return {
- touched: true,
- lastClientY: clientY,
- backdropOpacity,
- photoScale: 1,
- };
- }
- const offsetClientY = Math.abs(clientY - lastClientY);
- return {
- touched: true,
- lastClientY,
- backdropOpacity: Math.max(
- Math.min(defaultOpacity, defaultOpacity - offsetClientY / 100 / 2),
- 0,
- ),
- photoScale: Math.max(Math.min(1, 1 - offsetClientY / 100 / 10), 0.6),
- };
- });
- };
- handleReachHorizontalMove = clientX => {
- const { innerWidth } = window;
- this.setState(({ lastClientX, translateX, photoIndex }) => {
- if (lastClientX === undefined) {
- return {
- touched: true,
- lastClientX: clientX,
- translateX,
- };
- }
- const offsetClientX = clientX - lastClientX;
- return {
- touched: true,
- lastClientX: lastClientX,
- translateX:
- -(innerWidth + horizontalOffset) * photoIndex + offsetClientX,
- };
- });
- };
- handleIndexChange = (photoIndex: number) => {
- const singlePageWidth = window.innerWidth + horizontalOffset;
- const translateX = -singlePageWidth * photoIndex;
- this.setState({
- translateX,
- photoIndex,
- });
- const { onIndexChange } = this.props;
- if (onIndexChange) {
- onIndexChange(photoIndex);
- }
- };
- handleReachUp = (clientX: number, clientY: number) => {
- const { innerWidth, innerHeight } = window;
- const { images, onIndexChange, onClose } = this.props;
- const {
- lastClientX = clientX,
- lastClientY = clientY,
- photoIndex,
- overlayVisible,
- } = this.state;
- const offsetClientX = clientX - lastClientX;
- const offsetClientY = clientY - lastClientY;
- const singlePageWidth = innerWidth + horizontalOffset;
- // 当前偏移
- let currentTranslateX = -singlePageWidth * photoIndex;
- let currentPhotoIndex = photoIndex;
- let isChangeVisible = false;
- if (Math.abs(offsetClientY) > innerHeight * 0.14) {
- isChangeVisible = true;
- onClose();
- // 下一张
- } else if (
- offsetClientX < -maxMoveOffset &&
- photoIndex < images.length - 1
- ) {
- currentPhotoIndex = photoIndex + 1;
- currentTranslateX = -singlePageWidth * currentPhotoIndex;
- if (onIndexChange) {
- onIndexChange(currentPhotoIndex);
- }
- // 上一张
- } else if (offsetClientX > maxMoveOffset && photoIndex > 0) {
- currentPhotoIndex = photoIndex - 1;
- currentTranslateX = -singlePageWidth * currentPhotoIndex;
- if (onIndexChange) {
- onIndexChange(currentPhotoIndex);
- }
- }
- this.setState({
- touched: false,
- translateX: currentTranslateX,
- photoIndex: currentPhotoIndex,
- lastClientX: undefined,
- lastClientY: undefined,
- backdropOpacity: defaultOpacity,
- photoScale: 1,
- overlayVisible: isChangeVisible ? true : overlayVisible,
- });
- };
- render() {
- const {
- images,
- visible,
- className,
- maskClassName,
- viewClassName,
- imageClassName,
- onClose,
- bannerVisible,
- introVisible,
- overlayRender,
- loadingElement,
- brokenElement,
- } = this.props;
- const {
- translateX,
- touched,
- photoIndex,
- backdropOpacity,
- photoScale,
- overlayVisible,
- } = this.state;
- const imageLength = images.length;
- const transform = `translate3d(${translateX}px, 0px, 0)`;
- // Overlay
- const overlayIntro = imageLength ? images[photoIndex].intro : undefined;
- const overlayStyle = { opacity: +overlayVisible };
- if (visible) {
- const { innerWidth } = window;
- return (
- <SlideWrap className={className}>
- <div
- className={classNames(
- 'PhotoView-PhotoSlider__Backdrop',
- maskClassName,
- )}
- style={{ background: `rgba(0, 0, 0, ${backdropOpacity})` }}
- />
- {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={isMobile ? onClose : undefined}
- onClick={isMobile ? undefined : onClose}
- />
- </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}
- onReachTopMove={this.handleReachVerticalMove}
- onReachBottomMove={this.handleReachVerticalMove}
- onReachRightMove={
- realIndex < imageLength - 1
- ? this.handleReachHorizontalMove
- : undefined
- }
- onReachLeftMove={
- realIndex > 0 ? this.handleReachHorizontalMove : undefined
- }
- onReachUp={this.handleReachUp}
- onPhotoTap={this.handlePhotoTap}
- onMaskTap={this.handlePhotoMaskTap}
- photoScale={photoIndex === realIndex ? photoScale : 1}
- wrapClassName={viewClassName}
- className={imageClassName}
- style={{
- left: `${(innerWidth + horizontalOffset) * realIndex}px`,
- WebkitTransform: transform,
- transform,
- transition: touched
- ? undefined
- : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
- }}
- loadingElement={loadingElement}
- brokenElement={brokenElement}
- />
- );
- })}
- {introVisible && overlayIntro ? (
- <div
- className="PhotoView-PhotoSlider__FooterWrap"
- style={overlayStyle}
- >
- {overlayIntro}
- </div>
- ) : (
- undefined
- )}
- {overlayRender &&
- overlayRender({
- images,
- index: photoIndex,
- visible,
- onClose,
- onIndexChange: this.handleIndexChange,
- overlayVisible,
- })}
- </SlideWrap>
- );
- }
- return null;
- }
- }
|