Jelajahi Sumber

左右拖动,及下拉关闭

MinJieLiu 7 tahun lalu
induk
melakukan
6191870946
5 mengubah file dengan 246 tambahan dan 69 penghapusan
  1. 16 3
      examples/simple.tsx
  2. 131 15
      src/PhotoSlider.tsx
  3. 78 50
      src/PhotoView.tsx
  4. 1 1
      src/components/Backdrop.tsx
  5. 20 0
      src/variables.ts

+ 16 - 3
examples/simple.tsx

@@ -15,8 +15,9 @@ const Header = styled.header`
 
 class Example extends React.Component {
   state = {
-    photoImages: ['1.png', '2.jpg'],
+    photoImages: ['1.png', '2.jpg', '1.png'],
     photoVisible: true,
+    photoIndex: 1,
   };
 
   handlePhotoClose = () => {
@@ -25,13 +26,25 @@ class Example extends React.Component {
     });
   }
 
+  handleVisibleChange = (photoIndex) => {
+    this.setState({
+      photoIndex,
+    });
+  }
+
   render() {
-    const { photoImages, photoVisible } = this.state;
+    const { photoImages, photoVisible, photoIndex } = this.state;
 
     return (
       <Container>
         <Header>React 图片预览组件</Header>
-        <PhotoSlider images={photoImages} visible={photoVisible} onClose={this.handlePhotoClose} />
+        <PhotoSlider
+          images={photoImages}
+          index={photoIndex}
+          onIndexChange={this.handleVisibleChange}
+          visible={photoVisible}
+          onClose={this.handlePhotoClose}
+        />
       </Container>
     );
   }

+ 131 - 15
src/PhotoSlider.tsx

@@ -2,6 +2,7 @@ import React from 'react';
 import PhotoView from './PhotoView';
 import SlideWrap from './components/SlideWrap';
 import Backdrop from './components/Backdrop';
+import { maxMoveOffset, closePageOffset, defaultOpacity } from './variables';
 
 export interface IPhotoSliderProps {
   // 图片列表
@@ -14,11 +15,24 @@ export interface IPhotoSliderProps {
   onClose: Function;
   // 索引改变回调
   onIndexChange?: Function;
+  // 自定义容器
+  overlay?: React.ReactNode;
 }
 
 type PhotoSliderState = {
+  // 偏移量
   translateX: number;
+  // 图片当前的 index
   photoIndex: number;
+
+  // 图片处于触摸的状态
+  touched: boolean,
+  // Reach 开始时 x 坐标
+  lastPageX: number | undefined;
+  // Reach 开始时 y 坐标
+  lastPageY: number | undefined;
+  // 背景透明度
+  backdropOpacity: number;
 };
 
 export default class PhotoSlider extends React.Component<
@@ -27,61 +41,163 @@ export default class PhotoSlider extends React.Component<
 > {
   static displayName = 'PhotoSlider';
 
-  readonly state = {
+  static defaultProps = {
+    index: 0,
     translateX: 0,
-    photoIndex: 0,
   };
 
+  static getDerivedStateFromProps(nextProps, prevState) {
+    if (nextProps.index !== undefined && nextProps.index !== prevState.photoIndex) {
+      return {
+        photoIndex: nextProps.index,
+        translateX: window.innerWidth * nextProps.index,
+      };
+    }
+    return null;
+  }
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      translateX: props.index * window.innerWidth,
+      photoIndex: props.index || 0,
+      touched: false,
+
+      lastPageX: undefined,
+      lastPageY: undefined,
+      backdropOpacity: defaultOpacity,
+    };
+  }
+
   componentDidMount() {
+    window.addEventListener('resize', this.handleResize);
   }
 
   componentWillUnmount() {
+    window.removeEventListener('resize', this.handleResize);
+  }
+
+  handleResize = () => {
+    const { innerWidth } = window;
+    this.setState(({ photoIndex }) => {
+      return {
+        translateX: innerWidth * photoIndex,
+        lastPageX: undefined,
+        lastPageY: undefined,
+      };
+    });
   }
 
-  handleReachTopMove = () => {
+  handleReachTopMove = (pageX, pageY) => {
+    this.setState(({ lastPageY, backdropOpacity }) => {
+      if (lastPageY === undefined) {
+        return {
+          touched: true,
+          lastPageY: pageY,
+          backdropOpacity,
+        };
+      }
+      const offsetPageY = pageY - lastPageY;
+      return {
+        touched: true,
+        lastPageY,
+        backdropOpacity: Math.max(Math.min(0.6, 0.6 - (offsetPageY / 100)), 0.2),
+      };
+    });
   }
 
-  handleReachLeftMove = () => {
-    this.setState({
-      photoIndex: 0,
+  handleReachHorizontalMove = (pageX) => {
+    const { innerWidth } = window;
+    this.setState(({ lastPageX, translateX, photoIndex }) => {
+      if (lastPageX === undefined) {
+        return {
+          touched: true,
+          lastPageX: pageX,
+          translateX,
+        };
+      }
+      const offsetPageX = pageX - lastPageX;
+      return {
+        touched: true,
+        lastPageX: lastPageX,
+        translateX: innerWidth * photoIndex - offsetPageX,
+      };
     });
   }
 
-  handleReachRightMove = () => {
+  handleReachUp = (pageX, pageY) => {
+    const { innerWidth } = window;
+    const { images, onIndexChange, onClose } = this.props;
+    const { lastPageX = pageX, lastPageY = pageY, photoIndex } = this.state;
+
+    const offsetPageX = pageX - lastPageX;
+    const offsetPageY = pageY - lastPageY;
+
+    if (offsetPageY > closePageOffset) {
+      onClose();
+      return;
+    }
+    // 当前偏移
+    let currentTranslateX = innerWidth * photoIndex;
+    let currentPhotoIndex = photoIndex;
+    // 下一张
+    if (offsetPageX < - maxMoveOffset && photoIndex < images.length - 1) {
+      currentPhotoIndex = photoIndex + 1;
+      currentTranslateX = innerWidth * currentPhotoIndex;
+      if (onIndexChange) {
+        onIndexChange(currentPhotoIndex);
+      }
+      // 上一张
+    } else if (offsetPageX > maxMoveOffset && photoIndex > 0) {
+      currentPhotoIndex = photoIndex - 1;
+      currentTranslateX = innerWidth * currentPhotoIndex;
+      if (onIndexChange) {
+        onIndexChange(currentPhotoIndex);
+      }
+    }
     this.setState({
-      photoIndex: 1,
+      touched: false,
+      translateX: currentTranslateX,
+      photoIndex: currentPhotoIndex,
+      lastPageX: undefined,
+      lastPageY: undefined,
+      backdropOpacity: defaultOpacity,
     });
   }
 
   render() {
-    const { images, visible } = this.props;
-    const { photoIndex } = this.state;
+    const { images, visible, overlay } = this.props;
+    const { translateX, touched, backdropOpacity } = this.state;
     const { innerWidth } = window;
+    const transform = `translate3d(-${translateX}px, 0px, 0)`;
 
     if (visible) {
       return (
         <SlideWrap>
-          <Backdrop />
+          <Backdrop style={{ opacity: backdropOpacity }} />
           {images.map((src, index) => {
-            const transform = `translate3d(-${photoIndex * innerWidth}px, 0px, 0)`;
             return (
               <PhotoView
                 key={src + index}
                 src={src}
                 onReachTopMove={this.handleReachTopMove}
                 onReachRightMove={index < images.length - 1
-                  ? this.handleReachRightMove
+                  ? this.handleReachHorizontalMove
                   : undefined}
-                onReachLeftMove={index > 0 ? this.handleReachLeftMove : undefined}
+                onReachLeftMove={index > 0 ? this.handleReachHorizontalMove : undefined}
+                onReachUp={this.handleReachUp}
                 style={{
                   left: `${innerWidth * index}px`,
                   WebkitTransform: transform,
                   transform,
-                  transition: 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
+                  transition: touched
+                    ? undefined
+                    : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
                 }}
               />
             );
           })}
+          {overlay}
         </SlideWrap>
       );
     }

+ 78 - 50
src/PhotoView.tsx

@@ -8,7 +8,9 @@ import getMultipleTouchPosition from './utils/getMultipleTouchPosition';
 import getPositionOnMoveOrScale from './utils/getPositionOnMoveOrScale';
 import slideToSuitableOffset from './utils/slideToSuitableOffset';
 import { getClosedHorizontal, getClosedVertical } from './utils/getCloseEdge';
-import { defaultAnimationConfig } from './variables';
+import { defaultAnimationConfig, minReachOffset } from './variables';
+
+type ReachFunction = (pageX: number, pageY: number) => void;
 
 export interface IPhotoViewProps {
   // 图片地址
@@ -19,17 +21,19 @@ export interface IPhotoViewProps {
   className?: string;
   // style
   style?: object;
-  // 自定义容器
-  overlay?: React.ReactNode;
 
   // 到达顶部滑动事件
-  onReachTopMove?: Function;
+  onReachTopMove?: ReachFunction;
   // 到达右部滑动事件
-  onReachRightMove?: Function;
+  onReachRightMove?: ReachFunction;
   // 到达底部滑动事件
-  onReachBottomMove?: Function;
+  onReachBottomMove?: ReachFunction;
   // 到达左部滑动事件
-  onReachLeftMove?: Function;
+  onReachLeftMove?: ReachFunction;
+  // 触摸解除事件
+  onReachUp?: ReachFunction;
+
+  onPhotoResize?: () => void;
 }
 
 const initialState = {
@@ -119,8 +123,12 @@ export default class PhotoView extends React.Component<
         scale,
         lastTouchLength,
       }) => {
+        let currentX = x;
+        let currentY = y;
         if (touchLength === 0) {
-          const isStopMove = this.handleReachCallback(x, y, scale);
+          currentX = newPageX - pageX + lastX;
+          currentY = newPageY - pageY + lastY;
+          const isStopMove = this.handleReachCallback(currentX, currentY, scale, newPageX, newPageY);
           if (isStopMove) {
             return null;
           }
@@ -130,8 +138,8 @@ export default class PhotoView extends React.Component<
         return {
           lastTouchLength: touchLength,
           ...getPositionOnMoveOrScale({
-            x: touchLength ? x : newPageX - pageX + lastX,
-            y: touchLength ? y : newPageY - pageY + lastY,
+            x: currentX,
+            y: currentY,
             pageX: newPageX,
             pageY: newPageY,
             fromScale: scale,
@@ -154,7 +162,7 @@ export default class PhotoView extends React.Component<
           pageX,
           pageY,
           fromScale: scale,
-          toScale: scale !== 1 ? 1 : 2,
+          toScale: scale !== 1 ? 1 : 4,
         }),
         animation: defaultAnimationConfig,
       };
@@ -211,33 +219,39 @@ export default class PhotoView extends React.Component<
   }
 
   handleUp = (newPageX: number, newPageY: number) => {
-    const { width, height } = this.photoRef.state;
-    this.setState(({
-      x,
-      y,
-      lastX,
-      lastY,
-      scale,
-      touchedTime,
-      pageX,
-      pageY,
-    }) => {
-      const hasMove = pageX !== newPageX || pageY !== newPageY;
-      return {
-        touched: false,
-        ...slideToSuitableOffset({
-          x,
-          y,
-          lastX,
-          lastY,
-          width,
-          height,
-          scale,
-          touchedTime,
-          hasMove,
-        }),
-      };
-    });
+    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;
+        return {
+          touched: false,
+          ...slideToSuitableOffset({
+            x,
+            y,
+            lastX,
+            lastY,
+            width,
+            height,
+            scale,
+            touchedTime,
+            hasMove,
+          }),
+        };
+      });
+    }
   }
 
   handleTouchEnd = (e) => {
@@ -252,25 +266,40 @@ export default class PhotoView extends React.Component<
 
   handleResize = () => {
     this.setState(initialState);
+    const { onPhotoResize } = this.props;
+    if (onPhotoResize) {
+      onPhotoResize();
+    }
   }
 
-  handleReachCallback = (x: number, y: number, scale: number): boolean => {
+  handleReachCallback = (
+    x: number,
+    y: number,
+    scale: number,
+    newPageX: number,
+    newPageY: number,
+  ): boolean => {
     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;
+    const {
+      onReachTopMove,
+      onReachRightMove,
+      onReachBottomMove,
+      onReachLeftMove,
+    } = this.props;
     //  触碰到边缘
-    if (verticalType && onReachTopMove && y > 0) {
-      onReachTopMove(y);
-    } else if (verticalType && onReachBottomMove && y < 0) {
-      onReachBottomMove(y);
-    } else if (horizontalType && onReachLeftMove && x > 0) {
-      onReachLeftMove(x);
+    if (horizontalType && onReachLeftMove && x > minReachOffset) {
+      onReachLeftMove(newPageX, newPageY);
       return true;
-    } else if (horizontalType && onReachRightMove && x < 0) {
-      onReachRightMove(x);
+    } else if (horizontalType && onReachRightMove && x < -minReachOffset) {
+      onReachRightMove(newPageX, newPageY);
       return true;
+    } else if (verticalType && onReachTopMove && y > minReachOffset) {
+      onReachTopMove(newPageX, newPageY);
+    } else if (verticalType && onReachBottomMove && y < -minReachOffset) {
+      onReachBottomMove(newPageX, newPageY);
     }
     return false;
   }
@@ -280,7 +309,7 @@ export default class PhotoView extends React.Component<
   }
 
   render() {
-    const { src, wrapClassName, className, style, overlay } = this.props;
+    const { src, wrapClassName, className, style } = this.props;
     const { x, y, scale, touched, animation } = this.state;
 
     return (
@@ -312,7 +341,6 @@ export default class PhotoView extends React.Component<
             );
           }}
         </Motion>
-        {overlay}
       </PhotoWrap>
     );
   }

+ 1 - 1
src/components/Backdrop.tsx

@@ -6,7 +6,7 @@ const Backdrop = styled.div`
   left: 0;
   width: 100%;
   height: 100%;
-  background: rgba(0, 0, 0, 0.4);
+  background: black;
   z-index: -1;
 `;
 

+ 20 - 0
src/variables.ts

@@ -6,6 +6,26 @@ import { springType } from './types';
 export const maxTouchTime: number = 200;
 
 /**
+ * 最大切换滑动距离
+ */
+export const maxMoveOffset: number = 40;
+
+/**
+ * 最小触发边缘距离
+ */
+export const minReachOffset: number = 40;
+
+/**
+ * 关闭页面触发距离
+ */
+export const closePageOffset: number = 60;
+
+/**
+ * 默认背景透明度
+ */
+export const defaultOpacity: number = 0.6;
+
+/**
  * 默认动画参数
  */
 export const defaultAnimationConfig: springType = {