MinJieLiu před 7 roky
rodič
revize
d1ca0cbe0b
6 změnil soubory, kde provedl 177 přidání a 86 odebrání
  1. 1 0
      package.json
  2. 21 9
      src/Photo.tsx
  3. 73 48
      src/PhotoView.tsx
  4. 10 0
      src/types.ts
  5. 63 25
      src/utils.ts
  6. 9 4
      src/variables.ts

+ 1 - 0
package.json

@@ -46,6 +46,7 @@
   },
   "dependencies": {
     "lodash.throttle": "^4.1.1",
+    "react-motion": "^0.5.2",
     "styled-components": "^3.0.1"
   }
 }

+ 21 - 9
src/Photo.tsx

@@ -1,18 +1,16 @@
 import React from 'react';
+import styled from 'styled-components';
 import Spinner from './Spinner';
-import { getSuitableImageSize } from './util';
+import { getSuitableImageSize } from './utils';
 
-export interface IPhotoProps {
+export interface IPhotoProps extends React.HTMLAttributes<any> {
   src: string;
   loadingElement?: JSX.Element;
   brokenElement?: JSX.Element;
 }
 
-type ImageProps = {
+type PhotoState = {
   loaded: boolean;
-};
-
-type PhotoState = ImageProps & {
   broken: boolean;
   naturalWidth: number;
   naturalHeight: number;
@@ -20,6 +18,15 @@ type PhotoState = ImageProps & {
   height: number;
 };
 
+const PhotoImage = styled.img`
+  will-change: transform;
+  cursor: -webkit-grab;
+
+  &:active {
+    cursor: -webkit-grabbing;
+  }
+`;
+
 export default class Photo extends React.Component<IPhotoProps, PhotoState> {
   static displayName = 'Photo';
 
@@ -41,7 +48,7 @@ export default class Photo extends React.Component<IPhotoProps, PhotoState> {
     currPhoto.onerror = this.handleImageBroken;
   }
 
-  handleImageLoaded = (e) => {
+  handleImageLoaded = e => {
     const { naturalWidth, naturalHeight } = e.target;
     this.setState({
       loaded: true,
@@ -58,13 +65,18 @@ export default class Photo extends React.Component<IPhotoProps, PhotoState> {
   }
 
   render() {
-    const { src, loadingElement, brokenElement, ...restProps } = this.props;
+    const {
+      src,
+      loadingElement,
+      brokenElement,
+      ...restProps
+    } = this.props;
     const { loaded, broken, width, height } = this.state;
 
     if (src && !broken) {
       if (loaded) {
         return (
-          <img
+          <PhotoImage
             src={src}
             width={width}
             height={height}

+ 73 - 48
src/PhotoView.tsx

@@ -1,10 +1,11 @@
 import React from 'react';
-import styled from 'styled-components';
+import { Motion, spring } from 'react-motion';
 import throttle from 'lodash.throttle';
 import Photo from './Photo';
 import { PhotoContainer, Backdrop } from './StyledElements';
-import { slideToPosition, jumpToSuitableOffset } from './util';
-import { animationDefault, animationTimeBase } from './variables';
+import { getPositionOnScale, jumpToSuitableOffset } from './utils';
+import { defaultAnimationConfig } from './variables';
+import { animationType } from './types';
 
 export interface IPhotoViewProps {
   src: string;
@@ -29,25 +30,14 @@ type PhotoViewState = {
   offsetY: number;
   // 触摸开始时时间
   touchedTime: number;
-  // 动画名称
-  animationName: string | null;
-  // 动画时间
-  animationTime: number;
-};
-
-const DragPhoto = styled(Photo)<React.HTMLAttributes<any>>`
-  will-change: transform;
-  cursor: -webkit-grab;
-
-  &:active {
-    cursor: -webkit-grabbing;
-  }
-`;
+} & animationType;
 
 export default class PhotoView extends React.Component<
   IPhotoViewProps,
   PhotoViewState
 > {
+  static displayName = 'PhotoView';
+
   readonly state = {
     x: 0,
     y: 0,
@@ -59,8 +49,8 @@ export default class PhotoView extends React.Component<
     offsetX: 0,
     offsetY: 0,
     touchedTime: 0,
-    animationName: animationDefault,
-    animationTime: animationTimeBase,
+
+    animation: defaultAnimationConfig,
   };
 
   private photoRef;
@@ -107,12 +97,36 @@ export default class PhotoView extends React.Component<
     }
   }
 
-  handleDoubleClick = () => {
-    this.setState(prevState => ({
-      scale: prevState.scale > 1 ? 1 : 3,
-      animationName: animationDefault,
-      animationTime: animationTimeBase,
-    }));
+  handleDoubleClick = (e) => {
+    const { pageX, pageY } = e;
+    this.setState(({ x, y, scale }) => {
+      const toScale = scale > 1 ? 1 : 4;
+      const { distanceX, distanceY } = getPositionOnScale({ x, y, pageX, pageY, toScale });
+      return {
+        x: distanceX,
+        y: distanceY,
+        pageX,
+        pageY,
+        scale: toScale,
+      };
+    });
+  }
+
+  handleWheel = (e) => {
+    const { pageX, pageY, deltaY } = e;
+    this.setState(({ x, y, scale }) => {
+      const toScale = scale + deltaY / 100;
+      const { distanceX, distanceY } = getPositionOnScale({ x, y, pageX, pageY, toScale });
+
+      return {
+        x: distanceX,
+        y: distanceY,
+        pageX,
+        pageY,
+        scale: toScale,
+        animation: defaultAnimationConfig,
+      };
+    });
   }
 
   handleTouchStart = e => {
@@ -133,7 +147,8 @@ export default class PhotoView extends React.Component<
     this.handleMove(e.pageX, e.pageY);
   }
 
-  handleMouseUp = () => {
+  handleMouseUp = (e) => {
+    const { pageX, pageY } = e;
     const { width, height } = this.photoRef.state;
     this.setState(({
       x,
@@ -142,20 +157,21 @@ export default class PhotoView extends React.Component<
       offsetY,
       scale,
       touchedTime,
+      ...restPrevState
     }) => {
+      const hasMove: boolean = pageX !== restPrevState.pageX || pageY !== restPrevState.pageY;
       return {
         touched: false,
         ...jumpToSuitableOffset({
+          x,
+          y,
+          offsetX,
+          offsetY,
           width,
           height,
           scale,
-          ...slideToPosition({
-            x,
-            y,
-            offsetX,
-            offsetY,
-            touchedTime,
-          }),
+          touchedTime,
+          hasMove,
         }),
       };
     });
@@ -167,26 +183,35 @@ export default class PhotoView extends React.Component<
 
   render() {
     const { src } = this.props;
-    const { x, y, scale, touched, animationName, animationTime } = this.state;
-    const transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
+    const { x, y, scale, touched, animation } = this.state;
+    const style = {
+      currX: touched ? x : spring(x, animation),
+      currY: touched ? y : spring(y, animation),
+      currScale: spring(scale, animation),
+    };
 
     return (
       <PhotoContainer>
         <Backdrop />
-        <DragPhoto
-          src={src}
-          innerRef={this.handlePhotoRef}
-          onDoubleClick={this.handleDoubleClick}
-          onMouseDown={this.handleMouseDown}
-          onTouchStart={this.handleTouchStart}
-          style={{
-            WebkitTransform: transform,
-            transform,
-            transition: touched
-              ? undefined
-              : `transform ${animationTime}ms ${animationName}`,
+        <Motion style={style}>
+          {({ currX, currY, currScale }) => {
+            const transform = `translate3d(${currX}px, ${currY}px, 0) scale(${currScale})`;
+            return (
+              <Photo
+                src={src}
+                ref={this.handlePhotoRef}
+                onDoubleClick={this.handleDoubleClick}
+                onMouseDown={this.handleMouseDown}
+                onTouchStart={this.handleTouchStart}
+                onWheel={this.handleWheel}
+                style={{
+                  WebkitTransform: transform,
+                  transform,
+                }}
+              />
+            );
           }}
-        />
+        </Motion>
       </PhotoContainer>
     );
   }

+ 10 - 0
src/types.ts

@@ -0,0 +1,10 @@
+export type springType = {
+  // 刚性
+  stiffness: number;
+  // 减震
+  damping: number;
+};
+
+export type animationType = {
+  animation: springType;
+};

+ 63 - 25
src/util.ts → src/utils.ts

@@ -1,4 +1,5 @@
-import { animationTimeBase } from './variables';
+import { animationType } from './types';
+import { maxTouchTime, defaultAnimationConfig } from './variables';
 
 /**
  * 获取图片合适的大小
@@ -42,6 +43,32 @@ export const getSuitableImageSize = (
   };
 };
 
+export const getPositionOnScale = ({
+  x,
+  y,
+  pageX,
+  pageY,
+  toScale,
+}: {
+  x: number;
+  y: number;
+  pageX: number;
+  pageY: number;
+  toScale: number;
+}): {
+  distanceX: number;
+  distanceY: number;
+} => {
+  const { innerWidth, innerHeight } = window;
+  const scale = toScale - 1;
+  const distanceX = (innerWidth / 2 - x - pageX) * scale;
+  const distanceY = (innerHeight / 2 - y - pageY) * scale;
+  return {
+    distanceX: Math.floor(distanceX),
+    distanceY: Math.floor(distanceY),
+  };
+};
+
 export const slideToPosition = ({
   x,
   y,
@@ -57,55 +84,66 @@ export const slideToPosition = ({
 }): {
   endX: number;
   endY: number;
-  slideTime: number;
-} => {
+} & animationType => {
   const moveTime = Date.now() - touchedTime;
   const speedX = (x - offsetX) / moveTime;
   const speedY = (y - offsetY) / moveTime;
   const maxSpeed = Math.max(speedX, speedY);
-  const currentAnimationTime =
-    Math.abs(maxSpeed < 200 ? maxSpeed : 200) * animationTimeBase +
-    animationTimeBase;
+  const slideTime = moveTime < maxTouchTime ? Math.abs(maxSpeed) * 10 + 400 : 0;
   return {
-    endX: Math.floor(x + speedX * currentAnimationTime),
-    endY: Math.floor(y + speedY * currentAnimationTime),
-    slideTime: Math.floor(currentAnimationTime),
+    endX: Math.floor(x + speedX * slideTime),
+    endY: Math.floor(y + speedY * slideTime),
+    animation: {
+      stiffness: 170,
+      damping: 32,
+    },
   };
 };
 
 /**
  * 跳转到合适的图片偏移量
- * @param endX x 滑动后距离
- * @param endY y 滑动后距离
- * @param width 图片宽度
- * @param height 图片高度
- * @param scale 缩放
- * @param slideTime 动画时间
- * @return 坐标
  */
 export const jumpToSuitableOffset = ({
-  endX,
-  endY,
+  x,
+  y,
+  offsetX,
+  offsetY,
   width,
   height,
   scale,
-  slideTime,
+  touchedTime,
+  hasMove,
 }: {
-  endX: number;
-  endY: number;
+  x: number;
+  y: number;
+  offsetX: number;
+  offsetY: number;
   width: number;
   height: number;
   scale: number;
-  slideTime: number;
+  touchedTime: number;
+  hasMove: boolean;
 }): {
   x: number;
   y: number;
-  animationTime: 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, offsetX, offsetY, touchedTime });
+
   let currentX = endX;
   let currentY = endY;
 
@@ -129,6 +167,6 @@ export const jumpToSuitableOffset = ({
   return {
     x: currentX,
     y: currentY,
-    animationTime: isSlide ? slideTime : animationTimeBase,
+    animation: isSlide ? defaultAnimationConfig : animation,
   };
 };

+ 9 - 4
src/variables.ts

@@ -1,9 +1,14 @@
+import { springType } from './types';
+
 /**
- * 默认动画
+ * 最大触摸时间
  */
-export const animationDefault: string = 'cubic-bezier(0.25, 0.8, 0.25, 1)';
+export const maxTouchTime: number = 200;
 
 /**
- * 基础动画时间
+ * 默认动画参数
  */
-export const animationTimeBase: number = 400;
+export const defaultAnimationConfig: springType = {
+  stiffness: 240,
+  damping: 30,
+};