liumingyi_1 5 rokov pred
rodič
commit
be00aedbd1

+ 1 - 1
example/src/App.tsx

@@ -43,7 +43,7 @@ class App extends React.Component {
           <ImageList>
             {photoImages.map((item, index) => (
               <PhotoConsumer key={index} src={item} intro={item}>
-                {index < 2 ? <SmallImage src={item} /> : undefined}
+                <SmallImage />
               </PhotoConsumer>
             ))}
           </ImageList>

+ 19 - 15
src/Photo.tsx

@@ -7,19 +7,20 @@ import './Photo.less';
 
 export interface IPhotoProps extends React.HTMLAttributes<any> {
   src: string;
+  loaded: boolean;
+  naturalWidth: number;
+  naturalHeight: number;
+  width: number;
+  height: number;
   className?: string;
   onPhotoResize: () => void;
+  onImageLoad: (PhotoParams, callback?: Function) => void;
   loadingElement?: JSX.Element;
   brokenElement?: JSX.Element;
 }
 
 type PhotoState = {
-  loaded: boolean;
   broken: boolean;
-  naturalWidth: number;
-  naturalHeight: number;
-  width: number;
-  height: number;
 };
 
 export default class Photo extends React.PureComponent<
@@ -29,12 +30,7 @@ export default class Photo extends React.PureComponent<
   static displayName = 'Photo';
 
   readonly state = {
-    loaded: false,
     broken: false,
-    naturalWidth: 1,
-    naturalHeight: 1,
-    width: 1,
-    height: 1,
   };
 
   private isMount = true;
@@ -62,7 +58,8 @@ export default class Photo extends React.PureComponent<
   handleImageLoaded = e => {
     const { naturalWidth, naturalHeight } = e.target;
     if (this.isMount) {
-      this.setState({
+      const { onImageLoad } = this.props;
+      onImageLoad({
         loaded: true,
         naturalWidth,
         naturalHeight,
@@ -80,10 +77,10 @@ export default class Photo extends React.PureComponent<
   };
 
   handleResize = () => {
-    const { loaded, naturalWidth, naturalHeight } = this.state;
+    const { loaded, naturalWidth, naturalHeight } = this.props;
     if (loaded && this.isMount) {
-      const { onPhotoResize } = this.props;
-      this.setState(
+      const { onPhotoResize, onImageLoad } = this.props;
+      onImageLoad(
         getSuitableImageSize(naturalWidth, naturalHeight),
         onPhotoResize,
       );
@@ -93,13 +90,20 @@ export default class Photo extends React.PureComponent<
   render() {
     const {
       src,
+      loaded,
+      width,
+      height,
       className,
       loadingElement,
       brokenElement,
+      // ignore
+      naturalWidth,
+      naturalHeight,
       onPhotoResize,
+      onImageLoad,
       ...restProps
     } = this.props;
-    const { loaded, broken, width, height } = this.state;
+    const { broken } = this.state;
 
     if (src && !broken) {
       if (loaded) {

+ 9 - 2
src/PhotoConsumer.tsx

@@ -19,10 +19,16 @@ const PhotoConsumer: React.FC<IPhotoConsumer> = ({ src, intro, children }) => {
     clientX: undefined,
     clientY: undefined,
   });
+  const photoTriggerRef = React.useRef<HTMLElement | null>(null);
 
   React.useEffect(
     () => {
-      photoContext.addItem(key, src, intro);
+      photoContext.addItem({
+        key,
+        src,
+        originRef: photoTriggerRef.current,
+        intro,
+      });
       return () => {
         photoContext.removeItem(key);
       };
@@ -75,8 +81,9 @@ const PhotoConsumer: React.FC<IPhotoConsumer> = ({ src, intro, children }) => {
           ? {
               onTouchStart: handleTouchStart,
               onTouchEnd: handleTouchEnd,
+              ref: photoTriggerRef,
             }
-          : { onClick: handleClick },
+          : { onClick: handleClick, ref: photoTriggerRef },
       ),
     );
   }

+ 2 - 6
src/PhotoProvider.tsx

@@ -37,13 +37,9 @@ export default class PhotoProvider extends React.Component<
     };
   }
 
-  handleAddItem = (key: string, src: string, intro: React.ReactNode) => {
+  handleAddItem: addItemType = imageItem => {
     this.setState(prev => ({
-      images: prev.images.concat({
-        key,
-        src,
-        intro,
-      }),
+      images: prev.images.concat(imageItem),
     }));
   };
 

+ 19 - 0
src/PhotoSlider.less

@@ -1,3 +1,12 @@
+@keyframes PhotoView__fade {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
 .PhotoView {
   &-PhotoSlider__Backdrop {
     position: absolute;
@@ -9,6 +18,16 @@
     z-index: -1;
   }
 
+  &-PhotoSlider__fadeIn {
+    opacity: 0;
+    animation: PhotoView__fade 0.4s linear both;
+  }
+
+  &-PhotoSlider__fadeOut {
+    opacity: 0;
+    animation: PhotoView__fade 0.4s linear both reverse;
+  }
+
   &-PhotoSlider__BannerWrap {
     position: absolute;
     left: 0;

+ 118 - 118
src/PhotoSlider.tsx

@@ -3,11 +3,17 @@ 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 isMobile from './utils/isMobile';
-import './PhotoSlider.less';
-import { dataType, IPhotoProviderBase, ReachTypeEnum } from './types';
+import {
+  dataType,
+  IPhotoProviderBase,
+  ReachTypeEnum,
+  ShowAnimateEnum,
+} from './types';
 import { defaultOpacity, horizontalOffset, maxMoveOffset } from './variables';
+import './PhotoSlider.less';
 
 export interface IPhotoSliderProps extends IPhotoProviderBase {
   // 图片列表
@@ -87,39 +93,12 @@ export default class PhotoSlider extends React.Component<
     this.handlePhotoMaskTap = debounce(this.handlePhotoMaskTap, 200);
   }
 
-  private computeMousePosition: { x: number; y: number } | null; // 保存触发位置
-  private mousePositionEventBind: boolean = false; // 点击事件状态
-
   componentDidMount() {
     const { index = 0 } = this.props;
     this.setState({
       translateX: index * -(window.innerWidth + horizontalOffset),
       photoIndex: index,
     });
-
-    if (this.mousePositionEventBind) {
-      return;
-    }
-    // 只有点击事件支持从鼠标位置动画展开
-    let thisTimeOut;
-    document.addEventListener(
-      'click',
-      e => {
-        this.computeMousePosition = {
-          x: e.pageX,
-          y: e.pageY,
-        };
-        // 20ms 内发生过点击事件,则从点击位置动画展示
-        // 否则直接动画展示
-        // 这样可以兼容非点击方式展开
-        clearTimeout(thisTimeOut);
-        thisTimeOut = setTimeout(() => {
-          this.computeMousePosition = null;
-        }, 20);
-      },
-      true,
-    );
-    this.mousePositionEventBind = true;
   }
 
   handleClose = () => {
@@ -311,99 +290,120 @@ export default class PhotoSlider extends React.Component<
       overlayVisible,
     } = this.state;
     const imageLength = images.length;
+    const currentImage = images.length ? images[photoIndex] : undefined;
     const transform = `translate3d(${translateX}px, 0px, 0)`;
     // Overlay
-    const overlayIntro = imageLength ? images[photoIndex].intro : undefined;
-    const overlayStyle = { opacity: +overlayVisible };
+    const overlayIntro = currentImage && currentImage.intro;
 
-    if (visible) {
-      const { innerWidth } = window;
+    return (
+      <VisibleAnimationHandle visible={visible} currentImage={currentImage}>
+        {({ photoVisible, showAnimateType, originRect, onShowAnimateEnd }) => {
+          if (photoVisible) {
+            const { innerWidth } = window;
+            const overlayStyle = {
+              opacity:
+                overlayVisible && showAnimateType === ShowAnimateEnum.None
+                  ? 1
+                  : 0,
+            };
 
-      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}
-                  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
-                      ? undefined
-                      : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
-                  }}
-                  loadingElement={loadingElement}
-                  brokenElement={brokenElement}
-                  onPhotoResize={this.handleResize}
-                  transformOrigin={this.computeMousePosition
-                    ? `${this.computeMousePosition.x}px ${this.computeMousePosition.y}px`
-                    : undefined}
+            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, ${backdropOpacity})` }}
                 />
-              );
-            })}
-          {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;
+                {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}
+                        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
+                            ? undefined
+                            : 'transform 0.6s cubic-bezier(0.25, 0.8, 0.25, 1)',
+                        }}
+                        loadingElement={loadingElement}
+                        brokenElement={brokenElement}
+                        onPhotoResize={this.handleResize}
+                        showAnimateType={showAnimateType}
+                        originRect={originRect}
+                        onShowAnimateEnd={onShowAnimateEnd}
+                      />
+                    );
+                  })}
+                {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;
+        }}
+      </VisibleAnimationHandle>
+    );
   }
 }

+ 19 - 3
src/PhotoView.less

@@ -1,7 +1,7 @@
 @keyframes PhotoView__animateIn {
   from {
-    opacity: 0;
-    transform: scale(0.9);
+    opacity: 0.4;
+    transform: scale(0.2);
   }
   to {
     opacity: 1;
@@ -9,13 +9,29 @@
   }
 }
 
+@keyframes PhotoView__animateOut {
+  from {
+    opacity: 1;
+    transform: scale(1);
+  }
+  to {
+    opacity: 0;
+    transform: scale(0.2);
+  }
+}
+
 .PhotoView {
 
   &__animateIn {
-    opacity: 0;
+    opacity: 0.4;
     animation: PhotoView__animateIn 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) both;
   }
 
+  &__animateOut {
+    opacity: 1;
+    animation: PhotoView__animateOut 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) both;
+  }
+
   &__PhotoWrap {
     position: absolute;
     top: 0;

+ 100 - 54
src/PhotoView.tsx

@@ -8,6 +8,7 @@ import getPositionOnMoveOrScale from './utils/getPositionOnMoveOrScale';
 import slideToPosition from './utils/slideToPosition';
 import { getReachType, getCloseEdgeResult } from './utils/getCloseEdge';
 import withContinuousTap, { TapFuncType } from './utils/withContinuousTap';
+import getAnimateOrigin from './utils/getAnimateOrigin';
 import {
   maxScale,
   minStartTouchOffset,
@@ -20,6 +21,8 @@ import {
   PhotoTapFunction,
   ReachTypeEnum,
   TouchStartEnum,
+  ShowAnimateEnum,
+  OriginRectType,
 } from './types';
 import './PhotoView.less';
 
@@ -37,8 +40,6 @@ export interface IPhotoViewProps {
   // 加载失败 Element
   brokenElement?: JSX.Element;
 
-  transformOrigin: string | undefined;
-
   // Photo 点击事件
   onPhotoTap: PhotoTapFunction;
   // Mask 点击事件
@@ -47,11 +48,29 @@ export interface IPhotoViewProps {
   onReachMove: ReachMoveFunction;
   // 触摸解除事件
   onReachUp: ReachFunction;
-
+  // Resize 事件
   onPhotoResize?: () => void;
+
+  // 动画类型
+  showAnimateType?: ShowAnimateEnum;
+  // 动画源位置
+  originRect?: OriginRectType;
+  // 进入或结束动画回调
+  onShowAnimateEnd?: () => void;
 }
 
 const initialState = {
+  // 真实宽度
+  naturalWidth: 1,
+  // 真实高度
+  naturalHeight: 1,
+  // 宽度
+  width: 1,
+  // 高度
+  height: 1,
+  // 加载成功状态
+  loaded: false,
+
   // 图片 X 偏移量
   x: 0,
   // 图片 y 偏移量
@@ -92,7 +111,6 @@ export default class PhotoView extends React.Component<
   // 初始响应状态
   private initialTouchState = TouchStartEnum.Normal;
 
-  private readonly photoRef = React.createRef<Photo>();
   private readonly handlePhotoTap: TapFuncType<number>;
 
   constructor(props: IPhotoViewProps) {
@@ -125,6 +143,12 @@ export default class PhotoView extends React.Component<
     }
   }
 
+  handleImageLoad = (imageParams, callback) => {
+    this.setState({
+      ...imageParams,
+    }, callback);
+  };
+
   handleStart = (clientX: number, clientY: number, touchLength: number = 0) => {
     this.setState(prevState => ({
       touched: true,
@@ -139,6 +163,9 @@ export default class PhotoView extends React.Component<
 
   onMove = (newClientX: number, newClientY: number, touchLength: number = 0) => {
     const {
+      width,
+      height,
+      naturalWidth,
       x,
       y,
       clientX,
@@ -151,8 +178,7 @@ export default class PhotoView extends React.Component<
       touched,
       maskTouched,
     } = this.state;
-    const { current } = this.photoRef;
-    if ((touched || maskTouched) && current) {
+    if (touched || maskTouched) {
       // 单指最小缩放下,以初始移动距离来判断意图
       if (touchLength === 0 && scale === minScale && this.initialTouchState === TouchStartEnum.Normal) {
         const isBeyondX = Math.abs(newClientX - clientX) > minStartTouchOffset;
@@ -169,7 +195,6 @@ export default class PhotoView extends React.Component<
             : TouchStartEnum.YPush;
       }
 
-      const { width, height, naturalWidth } = current.state;
       let currentX = x;
       let currentY = y;
       // 边缘触发状态
@@ -243,11 +268,37 @@ export default class PhotoView extends React.Component<
   };
 
   onDoubleTap: TapFuncType<number> = (clientX, clientY) => {
-    const { current } = this.photoRef;
-    if (current) {
-      const { width, naturalWidth } = current.state;
-      const { x, y, scale } = this.state;
-      this.setState({
+    const { width, naturalWidth } = this.state;
+    const { x, y, scale } = this.state;
+    this.setState({
+      clientX,
+      clientY,
+      ...getPositionOnMoveOrScale({
+        x,
+        y,
+        clientX,
+        clientY,
+        fromScale: scale,
+        // 若图片足够大,则放大适应的倍数
+        toScale: scale !== 1 ? 1 : Math.max(2, naturalWidth / width),
+      }),
+    });
+  };
+
+  handleWheel = (e) => {
+    const { clientX, clientY, deltaY } = e;
+    const { width, naturalWidth } = this.state;
+    this.setState(({ x, y, scale }) => {
+      const endScale = scale - deltaY / 100 / 2;
+      // 限制最大倍数和最小倍数
+      const toScale = Math.max(
+        Math.min(
+          endScale,
+          Math.max(maxScale, naturalWidth / width)
+        ),
+        minScale,
+      );
+      return {
         clientX,
         clientY,
         ...getPositionOnMoveOrScale({
@@ -256,42 +307,10 @@ export default class PhotoView extends React.Component<
           clientX,
           clientY,
           fromScale: scale,
-          // 若图片足够大,则放大适应的倍数
-          toScale: scale !== 1 ? 1 : Math.max(2, naturalWidth / width),
+          toScale,
         }),
-      });
-    }
-  };
-
-  handleWheel = (e) => {
-    const { current } = this.photoRef;
-    if (current) {
-      const { clientX, clientY, deltaY } = e;
-      const { width, naturalWidth } = current.state;
-      this.setState(({ x, y, scale }) => {
-        const endScale = scale - deltaY / 100 / 2;
-        // 限制最大倍数和最小倍数
-        const toScale = Math.max(
-          Math.min(
-            endScale,
-            Math.max(maxScale, naturalWidth / width)
-          ),
-          minScale,
-        );
-        return {
-          clientX,
-          clientY,
-          ...getPositionOnMoveOrScale({
-            x,
-            y,
-            clientX,
-            clientY,
-            fromScale: scale,
-            toScale,
-          }),
-        };
-      });
-    }
+      };
+    });
   };
 
   handleMaskStart = (clientX: number, clientY: number) => {
@@ -338,6 +357,9 @@ export default class PhotoView extends React.Component<
     // 重置响应状态
     this.initialTouchState = TouchStartEnum.Normal;
     const {
+      width,
+      height,
+      naturalWidth,
       x,
       y,
       lastX,
@@ -349,10 +371,8 @@ export default class PhotoView extends React.Component<
       touched,
       maskTouched,
     } = this.state;
-    const { current } = this.photoRef;
-    if ((touched || maskTouched) && current) {
+    if (touched || maskTouched) {
       const { onReachUp, onPhotoTap, onMaskTap } = this.props;
-      const { width, height, naturalWidth } = current.state;
       const hasMove = clientX !== newClientX || clientY !== newClientY;
       this.setState({
         touched: false,
@@ -419,9 +439,21 @@ export default class PhotoView extends React.Component<
       style,
       loadingElement,
       brokenElement,
-      transformOrigin,
+      showAnimateType,
+      originRect,
+      onShowAnimateEnd,
     } = this.props;
-    const { x, y, scale, touched } = this.state;
+    const {
+      width,
+      height,
+      naturalWidth,
+      naturalHeight,
+      loaded,
+      x,
+      y,
+      scale,
+      touched,
+    } = this.state;
 
     const transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
 
@@ -432,11 +464,24 @@ export default class PhotoView extends React.Component<
           onMouseDown={isMobile ? undefined : this.handleMaskMouseDown}
           onTouchStart={isMobile ? this.handleMaskTouchStart : undefined}
         />
-        <div className="PhotoView__animateIn" style={{ transformOrigin }}>
+        <div
+          className={classNames({
+            PhotoView__animateIn: loaded && showAnimateType === ShowAnimateEnum.In,
+            PhotoView__animateOut: loaded && showAnimateType === ShowAnimateEnum.Out,
+          })}
+          style={{
+            transformOrigin: loaded ? getAnimateOrigin(originRect, width, height) : undefined,
+          }}
+          onAnimationEnd={onShowAnimateEnd}
+        >
           <Photo
             className={className}
             src={src}
-            ref={this.photoRef}
+            width={width}
+            height={height}
+            naturalWidth={naturalWidth}
+            naturalHeight={naturalHeight}
+            loaded={loaded}
             onMouseDown={isMobile ? undefined : this.handleMouseDown}
             onTouchStart={isMobile ? this.handleTouchStart : undefined}
             onWheel={this.handleWheel}
@@ -448,6 +493,7 @@ export default class PhotoView extends React.Component<
                 ? undefined
                 : 'transform 0.5s cubic-bezier(0.25, 0.8, 0.25, 1)',
             }}
+            onImageLoad={this.handleImageLoad}
             loadingElement={loadingElement}
             brokenElement={brokenElement}
           />

+ 68 - 0
src/components/VisibleAnimationHandle.tsx

@@ -0,0 +1,68 @@
+import React from 'react';
+import { dataType, OriginRectType, ShowAnimateEnum } from '../types';
+
+interface VisibleHandleProps {
+  visible: boolean;
+  currentImage?: dataType;
+  children: ({
+    photoVisible,
+    showAnimateType,
+    originRect,
+    onShowAnimateEnd,
+  }: {
+    photoVisible: boolean;
+    showAnimateType: ShowAnimateEnum;
+    originRect: OriginRectType;
+    onShowAnimateEnd: () => void;
+  }) => JSX.Element | null;
+}
+
+export default function VisibleAnimationHandle({
+  visible,
+  currentImage,
+  children,
+}: VisibleHandleProps) {
+  const [photoVisible, updatePhotoVisible] = React.useState(visible);
+  const [showAnimateType, updateAnimateStatus] = React.useState<
+    ShowAnimateEnum
+  >(ShowAnimateEnum.None);
+  const [originRect, updateOriginRect] = React.useState<OriginRectType>();
+
+  function onShowAnimateEnd() {
+    updateAnimateStatus(ShowAnimateEnum.None);
+
+    // Close
+    if (showAnimateType === ShowAnimateEnum.Out) {
+      updatePhotoVisible(false);
+    }
+  }
+
+  React.useEffect(() => {
+    if (!currentImage) {
+      return;
+    }
+    const originRef = currentImage.originRef;
+    if (originRef) {
+      // 获取触发时节点位置
+      const { top, left, width, height } = originRef.getBoundingClientRect();
+      updateOriginRect({
+        clientX: left + width / 2,
+        clientY: top + height / 2,
+      });
+    }
+
+    if (visible) {
+      updateAnimateStatus(ShowAnimateEnum.In);
+      updatePhotoVisible(true);
+    } else {
+      updateAnimateStatus(ShowAnimateEnum.Out);
+    }
+  }, [visible]);
+
+  return children({
+    photoVisible,
+    showAnimateType,
+    originRect,
+    onShowAnimateEnd,
+  });
+}

+ 10 - 8
src/photo-context.ts

@@ -1,19 +1,21 @@
 import React from 'react';
+import { dataType } from './types';
 
 export type onShowType = (key?: string) => void;
 
-export type addItemType = (
-  key?: string,
-  src?: string,
-  intro?: React.ReactNode,
-) => void;
+export type addItemType = ({
+  key,
+  src,
+  originRef,
+  intro,
+}: dataType) => void;
 
 export type removeItemType = (key?: string) => void;
 
 export interface PhotoContextType {
-  onShow: onShowType,
-  addItem: addItemType,
-  removeItem: removeItemType,
+  onShow: onShowType;
+  addItem: addItemType;
+  removeItem: removeItemType;
 }
 
 export default React.createContext<PhotoContextType>({

+ 20 - 4
src/types.ts

@@ -8,6 +8,8 @@ export type dataType = {
   key?: string;
   // 图片地址
   src: string;
+  // 原触发 ref
+  originRef: HTMLElement | null;
   // 图片介绍
   intro?: React.ReactNode;
 };
@@ -59,10 +61,7 @@ export type ReachMoveFunction = (
   scale?: number,
 ) => void;
 
-export type ReachFunction = (
-  clientX: number,
-  clientY: number,
-) => void;
+export type ReachFunction = (clientX: number, clientY: number) => void;
 
 export type PhotoTapFunction = (clientX: number, clientY: number) => void;
 
@@ -94,3 +93,20 @@ export enum TouchStartEnum {
   YPush, // Y 轴往上
   YPull, // Y 轴往下
 }
+
+/**
+ * 动画类型
+ */
+export enum ShowAnimateEnum {
+  None, // 无
+  In, // 进入
+  Out, // 离开
+}
+
+/**
+ * 触发源位置
+ */
+export type OriginRectType = {
+  clientX: number;
+  clientY: number;
+} | undefined;

+ 18 - 0
src/utils/getAnimateOrigin.ts

@@ -0,0 +1,18 @@
+import { OriginRectType } from '../types';
+
+export default function getAnimateOrigin(
+  originRect: OriginRectType,
+  width: number,
+  height: number,
+): string | undefined {
+  if (originRect) {
+    const { innerWidth, innerHeight } = window;
+
+    const xOrigin = (width - innerWidth) / 2 + originRect.clientX;
+    const yOrigin = (height - innerHeight) / 2 + originRect.clientY;
+
+    return `${xOrigin}px ${yOrigin}px`;
+  }
+
+  return undefined;
+}