MinJieLiu 7 سال پیش
والد
کامیت
2454a83df6

+ 0 - 0
examples/example.png → examples/1.png


BIN
examples/2.jpg


+ 15 - 2
examples/simple.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import styled from 'styled-components';
-import { PhotoView } from '../src';
+import { PhotoSlider } from '../src';
 
 const Container = styled.div`
   font-size: 32px;
@@ -14,11 +14,24 @@ const Header = styled.header`
 `;
 
 class Example extends React.Component {
+  state = {
+    photoImages: ['1.png', '2.jpg'],
+    photoVisible: true,
+  };
+
+  handlePhotoClose = () => {
+    this.setState({
+      photoVisible: false,
+    });
+  }
+
   render() {
+    const { photoImages, photoVisible } = this.state;
+
     return (
       <Container>
         <Header>React 图片预览组件</Header>
-        <PhotoView src="example.png" />
+        <PhotoSlider images={photoImages} visible={photoVisible} onClose={this.handlePhotoClose} />
       </Container>
     );
   }

+ 2 - 2
src/Photo.tsx

@@ -1,8 +1,8 @@
 import React from 'react';
 import styled from 'styled-components';
 import throttle from 'lodash.throttle';
-import Spinner from './Spinner';
-import { getSuitableImageSize } from './utils';
+import Spinner from './components/Spinner';
+import getSuitableImageSize from './utils/getSuitableImageSize';
 
 export interface IPhotoProps extends React.HTMLAttributes<any> {
   src: string;

+ 0 - 25
src/PhotoElements.tsx

@@ -1,25 +0,0 @@
-import React from 'react';
-import styled from 'styled-components';
-
-export const PhotoContainer = styled.section`
-  position: fixed;
-  top: 0;
-  left: 0;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 100%;
-  height: 100%;
-  z-index: 2000;
-  overflow: hidden;
-`;
-
-export const Backdrop = styled.div`
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background: rgba(0, 0, 0, 0.4);
-  z-index: -1;
-`;

+ 31 - 21
src/PhotoSlider.tsx

@@ -1,13 +1,19 @@
 import React from 'react';
 import PhotoView from './PhotoView';
+import SlideWrap from './components/SlideWrap';
+import Backdrop from './components/Backdrop';
 
 export interface IPhotoSliderProps {
-  // 图片数组
-  imageList: string[];
+  // 图片列表
+  images: string[];
   // 图片当前索引
-  index: number;
+  index?: number;
+  // 可见
+  visible: boolean;
+  // 关闭事件
+  onClose: Function;
   // 索引改变回调
-  onIndexChange: Function;
+  onIndexChange?: Function;
 }
 
 type PhotoSliderState = {
@@ -38,22 +44,26 @@ export default class PhotoSlider extends React.Component<
   }
 
   render() {
-    const { imageList } = this.props;
-
-    return (
-      <div>
-        {imageList.map((src, index) => (
-          <PhotoView
-            key={index}
-            src={src}
-            onReachTopMove={this.handleReachTopMove}
-            onReachRightMove={index < imageList.length
-              ? this.handleReachRightMove
-              : undefined}
-            onReachLeftMove={index > 0 ? this.handleReachLeftMove : undefined}
-          />
-        ))}
-      </div>
-    );
+    const { images, visible } = this.props;
+
+    if (visible) {
+      return (
+        <SlideWrap>
+          <Backdrop />
+          {images.map((src, index) => (
+            <PhotoView
+              key={src + index}
+              src={src}
+              onReachTopMove={this.handleReachTopMove}
+              onReachRightMove={index < images.length
+                ? this.handleReachRightMove
+                : undefined}
+              onReachLeftMove={index > 0 ? this.handleReachLeftMove : undefined}
+            />
+          ))}
+        </SlideWrap>
+      );
+    }
+    return null;
   }
 }

+ 11 - 11
src/PhotoView.tsx

@@ -2,13 +2,11 @@ import React from 'react';
 import { Motion, spring } from 'react-motion';
 import throttle from 'lodash.throttle';
 import Photo from './Photo';
-import { PhotoContainer, Backdrop } from './PhotoElements';
-import {
-  isMobile,
-  getMultipleTouchPosition,
-  getPositionOnMoveOrScale,
-  slideToSuitableOffset,
-} from './utils';
+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 { defaultAnimationConfig } from './variables';
 
 export interface IPhotoViewProps {
@@ -18,6 +16,8 @@ export interface IPhotoViewProps {
   wrapClassName?: string;
   // 图片类名
   className?: string;
+  // 自定义容器
+  overlay?: React.ReactNode;
 
   // 到达顶部滑动事件
   onReachTopMove?: Function;
@@ -249,7 +249,7 @@ export default class PhotoView extends React.Component<
   }
 
   render() {
-    const { src, wrapClassName, className } = this.props;
+    const { src, wrapClassName, className, overlay } = this.props;
     const { x, y, scale, touched, animation } = this.state;
     const style = {
       currX: touched ? x : spring(x, animation),
@@ -258,8 +258,7 @@ export default class PhotoView extends React.Component<
     };
 
     return (
-      <PhotoContainer className={wrapClassName}>
-        <Backdrop />
+      <PhotoWrap className={wrapClassName}>
         <Motion style={style}>
           {({ currX, currY, currScale }) => {
             const transform = `translate3d(${currX}px, ${currY}px, 0) scale(${currScale})`;
@@ -281,7 +280,8 @@ export default class PhotoView extends React.Component<
             );
           }}
         </Motion>
-      </PhotoContainer>
+        {overlay}
+      </PhotoWrap>
     );
   }
 }

+ 13 - 0
src/components/Backdrop.tsx

@@ -0,0 +1,13 @@
+import styled from 'styled-components';
+
+const Backdrop = styled.div`
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.4);
+  z-index: -1;
+`;
+
+export default Backdrop;

+ 16 - 0
src/components/PhotoWrap.tsx

@@ -0,0 +1,16 @@
+import styled from 'styled-components';
+
+const PhotoWrap = styled.section`
+  position: absolute;
+  top: 0;
+  left: 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+  z-index: 10;
+  overflow: hidden;
+`;
+
+export default PhotoWrap;

+ 62 - 0
src/components/SlideWrap.tsx

@@ -0,0 +1,62 @@
+import React from 'react';
+import { createPortal } from 'react-dom';
+import styled from 'styled-components';
+
+const Container = styled.div`
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 2000;
+  overflow: hidden;
+`;
+
+export default class SlideWrap extends React.Component<{
+  children: any;
+}> {
+  static displayName = 'SlideWrap';
+
+  dialogNode;
+  originalOverflow;
+
+  constructor(props) {
+    super(props);
+
+    // 创建容器
+    this.dialogNode = document.createElement('section');
+    document.body.appendChild(this.dialogNode);
+  }
+
+  componentDidMount() {
+    this.preventScroll();
+  }
+
+  componentWillUnmount() {
+    this.allowScroll();
+    // 清除容器
+    document.body.removeChild(this.dialogNode);
+    this.dialogNode = undefined;
+  }
+
+  preventScroll = () => {
+    const { style } = document.body;
+    this.originalOverflow = style.overflow;
+    style.overflow = 'hidden';
+  }
+
+  allowScroll = () => {
+    const { style } = document.body;
+    style.overflow = this.originalOverflow;
+    this.originalOverflow = undefined;
+  }
+
+  render() {
+    const { children } = this.props;
+
+    return createPortal(
+      <Container>{children}</Container>,
+      this.dialogNode,
+    );
+  }
+}

+ 0 - 0
src/Spinner.tsx → src/components/Spinner.tsx


+ 1 - 0
src/index.tsx

@@ -1 +1,2 @@
 export { default as PhotoView } from './PhotoView';
+export { default as PhotoSlider } from './PhotoSlider';

+ 0 - 227
src/utils.ts

@@ -1,227 +0,0 @@
-import React from 'react';
-import { animationType } from './types';
-import { maxTouchTime, defaultAnimationConfig } from './variables';
-
-/**
- * 是否为移动端设备
- */
-export const isMobile: boolean = window.navigator.userAgent.includes('Mobile');
-
-/**
- * 从 Touch 事件中获取多个触控位置
- */
-export const getMultipleTouchPosition = (
-  evt: React.TouchEvent,
-): {
-  pageX: number;
-  pageY: number;
-  touchLength: number;
-} => {
-  const { pageX, pageY } = evt.touches[0];
-  const { pageX: nextPageX, pageY: nextPageY } = evt.touches[1];
-  return {
-    pageX: (pageX + nextPageX) / 2,
-    pageY: (pageY + nextPageY) / 2,
-    touchLength: Math.sqrt(
-      Math.pow(nextPageX - pageX, 2) + Math.pow(nextPageY - pageY, 2),
-    ),
-  };
-};
-
-/**
- * 获取图片合适的大小
- * @param naturalWidth 图片真实宽度
- * @param naturalHeight 图片真实高度
- * @return 图片合适的大小
- */
-export const getSuitableImageSize = (
-  naturalWidth: number,
-  naturalHeight: number,
-): {
-  width: number;
-  height: number;
-} => {
-  let width = 0;
-  let height = 0;
-  const { innerWidth, innerHeight } = window;
-  if (naturalWidth < innerWidth && naturalHeight < innerHeight) {
-    width = naturalWidth;
-    height = naturalHeight;
-  } else if (naturalWidth < innerWidth && naturalHeight >= innerHeight) {
-    width = (naturalWidth / naturalHeight) * innerHeight;
-    height = innerHeight;
-  } else if (naturalWidth >= innerWidth && naturalHeight < innerHeight) {
-    width = innerWidth;
-    height = (naturalHeight / naturalWidth) * innerWidth;
-  } else if (
-    naturalWidth >= innerWidth &&
-    naturalHeight >= innerHeight &&
-    naturalWidth / naturalHeight > innerWidth / innerHeight
-  ) {
-    width = innerWidth;
-    height = (naturalHeight / naturalWidth) * innerWidth;
-  } else {
-    width = (naturalWidth / naturalHeight) * innerHeight;
-    height = innerHeight;
-  }
-  return {
-    width: Math.floor(width),
-    height: Math.floor(height),
-  };
-};
-
-export const getPositionOnMoveOrScale = ({
-  x,
-  y,
-  pageX,
-  pageY,
-  fromScale,
-  toScale,
-}: {
-  x: number;
-  y: number;
-  pageX: number;
-  pageY: number;
-  fromScale: number;
-  toScale: number;
-}): {
-  x: number;
-  y: number;
-  scale: number;
-} => {
-  const { innerWidth, innerHeight } = window;
-  let endScale = toScale;
-  let originX = x;
-  let originY = y;
-  // 缩放限制
-  if (toScale < 1) {
-    endScale = 1;
-  } else if (toScale > 6) {
-    endScale = 6;
-  } else {
-    const centerPageX = innerWidth / 2;
-    const centerPageY = innerHeight / 2;
-    // 坐标偏移
-    const lastPositionX = centerPageX + x;
-    const lastPositionY = centerPageY + y;
-
-    // 放大偏移量
-    const offsetScale = endScale / fromScale;
-    // 偏移位置
-    originX = pageX - (pageX - lastPositionX) * offsetScale - centerPageX;
-    originY = pageY - (pageY - lastPositionY) * offsetScale - centerPageY;
-  }
-  return {
-    x: originX,
-    y: originY,
-    scale: endScale,
-  };
-};
-
-export const slideToPosition = ({
-  x,
-  y,
-  lastX,
-  lastY,
-  touchedTime,
-}: {
-  x: number;
-  y: number;
-  lastX: number;
-  lastY: number;
-  touchedTime: number;
-}): {
-  endX: number;
-  endY: number;
-} & animationType => {
-  const moveTime = Date.now() - touchedTime;
-  const speedX = (x - lastX) / moveTime;
-  const speedY = (y - lastY) / moveTime;
-  const maxSpeed = Math.max(speedX, speedY);
-  const slideTime = moveTime < maxTouchTime ? Math.abs(maxSpeed) * 20 + 400 : 0;
-  return {
-    endX: Math.floor(x + speedX * slideTime),
-    endY: Math.floor(y + speedY * slideTime),
-    animation: {
-      stiffness: 170,
-      damping: 32,
-    },
-  };
-};
-
-/**
- * 适应到合适的图片偏移量
- */
-export const slideToSuitableOffset = ({
-  x,
-  y,
-  lastX,
-  lastY,
-  width,
-  height,
-  scale,
-  touchedTime,
-  hasMove,
-}: {
-  x: number;
-  y: number;
-  lastX: number;
-  lastY: number;
-  width: number;
-  height: number;
-  scale: number;
-  touchedTime: number;
-  hasMove: boolean;
-}): {
-  x: number;
-  y: number;
-} & animationType => {
-  // 没有移动图片
-  if (!hasMove) {
-    return {
-      x,
-      y,
-      animation: defaultAnimationConfig,
-    };
-  }
-
-  const { innerWidth, innerHeight } = window;
-  // 图片超出的长度
-  const outOffsetX = (width * scale - innerWidth) / 2;
-  const outOffsetY = (height * scale - innerHeight) / 2;
-
-  // 滑动到结果的位置
-  const { endX, endY, animation } = slideToPosition({
-    x,
-    y,
-    lastX,
-    lastY,
-    touchedTime,
-  });
-
-  let currentX = endX;
-  let currentY = endY;
-
-  if (width * scale <= innerWidth) {
-    currentX = 0;
-  } else if (endX > 0 && outOffsetX - endX <= 0) {
-    currentX = outOffsetX;
-  } else if (endX < 0 && outOffsetX + endX <= 0) {
-    currentX = -outOffsetX;
-  }
-  if (height * scale <= innerHeight) {
-    currentY = 0;
-  } else if (endY > 0 && outOffsetY - endY <= 0) {
-    currentY = outOffsetY;
-  } else if (endY < 0 && outOffsetY + endY <= 0) {
-    currentY = -outOffsetY;
-  }
-
-  const isBumpEdge = currentX !== endX || currentY !== endY;
-
-  return {
-    x: currentX,
-    y: currentY,
-    animation: isBumpEdge ? defaultAnimationConfig : animation,
-  };
-};

+ 24 - 0
src/utils/getMultipleTouchPosition.ts

@@ -0,0 +1,24 @@
+import React from 'react';
+
+/**
+ * 从 Touch 事件中获取多个触控位置
+ */
+const getMultipleTouchPosition = (
+  evt: React.TouchEvent,
+): {
+  pageX: number;
+  pageY: number;
+  touchLength: number;
+} => {
+  const { pageX, pageY } = evt.touches[0];
+  const { pageX: nextPageX, pageY: nextPageY } = evt.touches[1];
+  return {
+    pageX: (pageX + nextPageX) / 2,
+    pageY: (pageY + nextPageY) / 2,
+    touchLength: Math.sqrt(
+      Math.pow(nextPageX - pageX, 2) + Math.pow(nextPageY - pageY, 2),
+    ),
+  };
+};
+
+export default getMultipleTouchPosition;

+ 52 - 0
src/utils/getPositionOnMoveOrScale.ts

@@ -0,0 +1,52 @@
+/**
+ * 获取移动或缩放之后的中心点
+ */
+const getPositionOnMoveOrScale = ({
+  x,
+  y,
+  pageX,
+  pageY,
+  fromScale,
+  toScale,
+}: {
+  x: number;
+  y: number;
+  pageX: number;
+  pageY: number;
+  fromScale: number;
+  toScale: number;
+}): {
+  x: number;
+  y: number;
+  scale: number;
+} => {
+  const { innerWidth, innerHeight } = window;
+  let endScale = toScale;
+  let originX = x;
+  let originY = y;
+  // 缩放限制
+  if (toScale < 1) {
+    endScale = 1;
+  } else if (toScale > 6) {
+    endScale = 6;
+  } else {
+    const centerPageX = innerWidth / 2;
+    const centerPageY = innerHeight / 2;
+    // 坐标偏移
+    const lastPositionX = centerPageX + x;
+    const lastPositionY = centerPageY + y;
+
+    // 放大偏移量
+    const offsetScale = endScale / fromScale;
+    // 偏移位置
+    originX = pageX - (pageX - lastPositionX) * offsetScale - centerPageX;
+    originY = pageY - (pageY - lastPositionY) * offsetScale - centerPageY;
+  }
+  return {
+    x: originX,
+    y: originY,
+    scale: endScale,
+  };
+};
+
+export default getPositionOnMoveOrScale;

+ 40 - 0
src/utils/getSuitableImageSize.ts

@@ -0,0 +1,40 @@
+/**
+ * 获取图片合适的大小
+ */
+const getSuitableImageSize = (
+  naturalWidth: number,
+  naturalHeight: number,
+): {
+  width: number;
+  height: number;
+} => {
+  let width = 0;
+  let height = 0;
+  const { innerWidth, innerHeight } = window;
+  if (naturalWidth < innerWidth && naturalHeight < innerHeight) {
+    width = naturalWidth;
+    height = naturalHeight;
+  } else if (naturalWidth < innerWidth && naturalHeight >= innerHeight) {
+    width = (naturalWidth / naturalHeight) * innerHeight;
+    height = innerHeight;
+  } else if (naturalWidth >= innerWidth && naturalHeight < innerHeight) {
+    width = innerWidth;
+    height = (naturalHeight / naturalWidth) * innerWidth;
+  } else if (
+    naturalWidth >= innerWidth &&
+    naturalHeight >= innerHeight &&
+    naturalWidth / naturalHeight > innerWidth / innerHeight
+  ) {
+    width = innerWidth;
+    height = (naturalHeight / naturalWidth) * innerWidth;
+  } else {
+    width = (naturalWidth / naturalHeight) * innerHeight;
+    height = innerHeight;
+  }
+  return {
+    width: Math.floor(width),
+    height: Math.floor(height),
+  };
+};
+
+export default getSuitableImageSize;

+ 6 - 0
src/utils/isMobile.ts

@@ -0,0 +1,6 @@
+/**
+ * 是否为移动端设备
+ */
+const isMobile: boolean = window.navigator.userAgent.includes('Mobile');
+
+export default isMobile;

+ 112 - 0
src/utils/slideToSuitableOffset.ts

@@ -0,0 +1,112 @@
+import { animationType } from '../types';
+import { maxTouchTime, defaultAnimationConfig } from '../variables';
+
+const slideToPosition = ({
+  x,
+  y,
+  lastX,
+  lastY,
+  touchedTime,
+}: {
+  x: number;
+  y: number;
+  lastX: number;
+  lastY: number;
+  touchedTime: number;
+}): {
+  endX: number;
+  endY: number;
+} & animationType => {
+  const moveTime = Date.now() - touchedTime;
+  const speedX = (x - lastX) / moveTime;
+  const speedY = (y - lastY) / moveTime;
+  const maxSpeed = Math.max(speedX, speedY);
+  const slideTime = moveTime < maxTouchTime ? Math.abs(maxSpeed) * 20 + 400 : 0;
+  return {
+    endX: Math.floor(x + speedX * slideTime),
+    endY: Math.floor(y + speedY * slideTime),
+    animation: {
+      stiffness: 170,
+      damping: 32,
+    },
+  };
+};
+
+/**
+ * 适应到合适的图片偏移量
+ */
+const slideToSuitableOffset = ({
+  x,
+  y,
+  lastX,
+  lastY,
+  width,
+  height,
+  scale,
+  touchedTime,
+  hasMove,
+}: {
+  x: number;
+  y: number;
+  lastX: number;
+  lastY: number;
+  width: number;
+  height: number;
+  scale: number;
+  touchedTime: number;
+  hasMove: boolean;
+}): {
+  x: number;
+  y: number;
+} & animationType => {
+  // 没有移动图片
+  if (!hasMove) {
+    return {
+      x,
+      y,
+      animation: defaultAnimationConfig,
+    };
+  }
+
+  const { innerWidth, innerHeight } = window;
+  // 图片超出的长度
+  const outOffsetX = (width * scale - innerWidth) / 2;
+  const outOffsetY = (height * scale - innerHeight) / 2;
+
+  // 滑动到结果的位置
+  const { endX, endY, animation } = slideToPosition({
+    x,
+    y,
+    lastX,
+    lastY,
+    touchedTime,
+  });
+
+  let currentX = endX;
+  let currentY = endY;
+
+  if (width * scale <= innerWidth) {
+    currentX = 0;
+  } else if (endX > 0 && outOffsetX - endX <= 0) {
+    currentX = outOffsetX;
+  } else if (endX < 0 && outOffsetX + endX <= 0) {
+    currentX = -outOffsetX;
+  }
+  if (height * scale <= innerHeight) {
+    currentY = 0;
+  } else if (endY > 0 && outOffsetY - endY <= 0) {
+    currentY = outOffsetY;
+  } else if (endY < 0 && outOffsetY + endY <= 0) {
+    currentY = -outOffsetY;
+  }
+
+  const isBumpEdge = currentX !== endX || currentY !== endY;
+
+  return {
+    x: currentX,
+    y: currentY,
+    animation: isBumpEdge ? defaultAnimationConfig : animation,
+  };
+};
+
+export default slideToSuitableOffset;