react hook封装购物车动画

作者: DoNow | 来源:发表于2020-04-22 23:26 被阅读0次

    前阵子,开发过程中需要用到购物车动画,所以封装了动画hooks,在此做一下总结归纳。

    一、思考

    首先,购物车动画的轨迹是一个抛物线效果,这个我们可以通过CSS动画来实现。其次,我们的抛物线需要一个起始点、一个目标点、一个运动小球
    然后,通过计算起始点和目标点两者之间 x 轴和 y 轴的距离,然后通过 CSS 来改变运动小球的位置和移动速度,从而实现加入购物车效果。

    思考框架.png

    那么,这个抛物线动画效果如何实现?

    高中物理告诉我们,当物体运动时,X轴方向上和Y轴方向上的速度不一致时,物体的运动效果就是抛物线,类似我们向外抛球,小球的运动轨迹。

    所以,想要有抛物线效果,我们只需要控制运动小球,从起始点运动到目标点的过程中,X轴和Y轴方向上的速度不一致即可。

    因此,我们可以通过X轴方向上的速度不变,通过Y轴方向上的速度变化。

    那么,如何控制Y轴上的速度变化?

    搜索前端 CSS 样式,我们可以发现,可以使用 transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
    属性来实现过渡效果的速度变化。

    其中,三阶贝塞尔曲线cubic-bezier(x1, y1, x2, y2): 四个参数值分别在 0 到 1 之间,其中 (x1, y1)(x2, y2) 是控制曲线的变化程度。

    快点击链接,去玩玩这个曲线吧!可好玩了!

    什么是贝塞尔曲线?快去了解它!!

    二、基本框架

    我们思考一下,想要把这个动画效果封装起来通用,我们需要传入哪些必传参数? 需要暴露哪些参数或方法给外层组件调用? 需要提供哪个参数便于个性化扩展?

    1. 需要起始Dom节点、目标Dom节点;
    2. 需要暴露running方法,用于开启动画效果;
    3. 需要运动小球,小球包含两层,外层flyOuter控制X轴匀速运动,内层flyInner控制Y轴变速运动;
    4. 需要提供属性,支持自定义小球的内容children、小球内外层样式 flyOuterStyle / flyInnerStyle 、小球运动时间设置runTime、小球开始运动回调beforeRun、小球开始运动回调afterRun

    hook封装实现

    import React, { useRef, useEffect, useImperativeHandle } from 'react';
    
    import ReactDOM from 'react-dom';
    
    /**
     * 动画球
     * @params children - 小球扩展内容
     * @params flyOuterStyle - 小球外层扩展样式
     * @params flyInnerStyle - 小球内层扩展样式
     * @params runTime - 小球运动时间
     * @params ref - 小球dom实例
     */
    const flyOuter = React.forwardRef(
      ({ children, flyOuterStyle = {}, flyInnerStyle = {}, runTime = 0.8 }, ref) => {
        const flyOuterRef = useRef();
        const flyInnerRef = useRef();
        useImperativeHandle(ref, () => ({ flyOuterRef, flyInnerRef }));
    
    
        // 运动小球外层样式
        const flyOuter_Style = Object.assign(
          {
            position: 'absolute',
            width: '20px',
            height: '20px',
            transition: `transform ${runTime}s`,
            display: 'none',
            margin: ' -20px 0 0 -20px',
            transitionTimingFunction: 'linear',
            zIndex: 3,
          },
          flyOuterStyle,
        );
    
        // 运动小球内层样式
        const flyInner_Style = Object.assign(
          {
            position: 'absolute',
            width: '100%',
            height: '100%',
            borderRadius: '50%',
            backgroundColor: '#FF8A2B',
            color: '#ffffff',
            textAlign: 'center',
            lineHeight: '1',
            transition: `transform ${runTime}s`,
            justifyContent: 'center',
            alignItems: 'center',
            // transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)', // 向上抛物线的右边
            transitionTimingFunction: 'cubic-bezier(0, 0, .25, 1.3)', // 向下抛物线的左边
          },
          flyInnerStyle,
        );
    
        return (
          <div style={flyOuter_Style} ref={flyOuterRef}>
            <div style={flyInner_Style} ref={flyInnerRef}>
              {children}
            </div>
          </div>
        );
      },
    );
    
    
    /**
     * 抛物线动画效果
     * @params startRef - 起始点dom节点
     * @params endRef - 目标点dom节点
     * @params flyOuterStyle - 小球外层扩展样式
     * @params flyInnerStyle - 小球内层扩展样式
     * @params runTime - 小球运动时间
     * @params beforeRun - 小球开始运动回调
     * @params afterRun - 小球结束运动回调
     * @params children - 小球扩展内容
     * @returns { running } - 小球开始运动函数
     */
    export default function useParabola(
      {
        startRef,
        endRef,
        flyOuterStyle,
        flyInnerStyle,
        runTime = 800,
        beforeRun = () => {},
        afterRun = () => {},
      },
      children,
    ) {
      const containerRef = useRef(document.createElement('div'));
      const innerRef = useRef();
      let isRunning = false;
    
      // 挂载到dom上
      useEffect(() => {
        const container = containerRef.current;
        document.body.appendChild(container);
        return () => {
          document.body.removeChild(container);
        };
      }, []);
    
    
      useEffect(() => {
        if (startRef?.current && endRef?.current) {
          ReactDOM.render(
            React.createElement(
              flyOuter,
              { ref: innerRef, flyOuterStyle, flyInnerStyle, runTime: runTime / 1000 },
              children,
            ),
            containerRef.current,
          );
        }
      }, [startRef, endRef]); // eslint-disable-line
    
      function running() {
        if (startRef && endRef && innerRef) {
          beforeRun();
          const flyOuterRef = innerRef.current.flyOuterRef.current;
          const flyInnerRef = innerRef.current.flyInnerRef.current;
    
          // 现在起点距离终点的距离
          const startDot = startRef.current.getBoundingClientRect();
          const endDot = endRef.current.getBoundingClientRect();
    
          // 中心点的水平垂直距离
          const offsetX = endDot.left + endDot.width / 4 - (startDot.left + startDot.width / 2);
          // let offsetY = endDot.top + endDot.height / 2 - (startDot.top + startDot.height / 2);
          const offsetY = endDot.top + endDot.height / 4 - (startDot.top + startDot.height / 2);
    
          // 页面滚动尺寸
          const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0;
          const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0;
          if (!isRunning) {
            // 初始定位
            flyOuterRef.style.display = 'block';
            flyOuterRef.style.left = `${
              startDot.left + scrollLeft + startRef.current.clientWidth / 2
            }px`;
            flyOuterRef.style.top = `${startDot.top + scrollTop + startRef.current.clientHeight / 2}px`;
    
            // 开始动画
            flyOuterRef.style.transform = `translateX(${offsetX}px)`;
            flyInnerRef.style.transform = `translateY(${offsetY}px)`;
    
            // 动画标志量
            isRunning = true;
            setTimeout(() => {
              flyOuterRef.style.display = 'none';
              flyOuterRef.style.left = '';
              flyOuterRef.style.top = '';
              flyOuterRef.style.transform = '';
              flyInnerRef.style.transform = '';
              isRunning = false;
    
              afterRun();
            }, runTime);
          }
        }
      }
    
      return { running };
    }
    

    三、测试用例

    实现效果:


    购物车动画.gif

    js代码

    import React, { useRef, useState } from 'react';
    import { Button, notification } from 'antd';
    import { ShoppingCartOutlined, PayCircleOutlined } from '@ant-design/icons';
    import useParabola from '@/hooks/use-parabola';
    import styles from './index.less';
    
    /*
     * @Description: 购物车动画-demo
     * @version: 0.0.1
     * @Date: 2020-04-20 23:21:33
     */
    export default React.forwardRef(() => {
      const [num, setNum] = useState(1);
    
      const startRef = useRef();
      const endRef_1 = useRef();
      const endRef_2 = useRef();
      const endRef_3 = useRef();
      const endRef_4 = useRef();
      const res_1 = useParabola(
        {
          startRef,
          endRef: endRef_1,
          flyOuterStyle: {
            width: '40px',
            height: '40px',
            transition: 'transform 3s',
            margin: ' -40px 0 0 -40px',
          },
          flyInnerStyle: {
            color: '#FF0000',
            transition: 'transform 3s',
            lineHeight: '40px',
          },
          runTime: 3000,
          beforeRun: () => {
            notification.warning({ message: '12号球开始运动啦啦~~' });
          },
          afterRun: () => {
            notification.success({ message: '12号球运动结束啦啦~~' });
          },
        },
        <span>12</span>,
      );
      const res_2 = useParabola(
        {
          startRef,
          endRef: endRef_2,
          flyInnerStyle: {
            transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)',
          },
        },
        '2',
      );
      const res_3 = useParabola(
        {
          startRef,
          endRef: endRef_3,
          flyOuterStyle: { transition: 'transform 2.5s' },
          flyInnerStyle: { transition: 'transform 2.5s' },
          runTime: 2500,
        },
        '3',
      );
      const res_4 = useParabola(
        {
          startRef,
          endRef: endRef_4,
          flyInnerStyle: {
            transitionTimingFunction: 'cubic-bezier(.55,0,.85,.36)',
          },
        },
        '4',
      );
    
      function startRunning() {
        if (num % 4 === 1) {
          res_1.running(1);
        }
        if (num % 4 === 2) {
          res_2.running(2);
        }
        if (num % 4 === 3) {
          res_3.running(3);
        }
        if (num % 4 === 0) {
          res_4.running(4);
        }
        setNum(num + 1);
      }
    
      return (
        <div className={styles['cart-animation']}>
          <div className={styles.center}>
            <div ref={startRef}>
              <Button danger icon={<PayCircleOutlined />} onClick={startRunning}>
                发射中心
              </Button>
            </div>
          </div>
    
          <div className={styles.left}>
            <div ref={endRef_1}>
              <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-top']}>
                购物车1号
              </Button>
            </div>
            <div ref={endRef_2}>
              <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-bottom']}>
                购物车2号
              </Button>
            </div>
          </div>
          <div className={styles.right}>
            <div ref={endRef_3}>
              <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-top']}>
                购物车3号
              </Button>
            </div>
            <div ref={endRef_4}>
              <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-bottom']}>
                购物车4号
              </Button>
            </div>
          </div>
        </div>
      );
    });
    

    css代码

    @import '~antd/lib/style/themes/default.less';
    
    .cart-animation {
      position: relative;
      height: 300px;
      .center {
        width: 100%;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
      }
      .left,
      .right {
        position: absolute;
        top: 50px;
      }
      .left {
        left: 0;
      }
      .right {
        right: 0;
      }
      .left-top,
      .right-top {
        margin-bottom: 200px;
      }
      button {
        display: block;
      }
    }
    
    

    四、参考链接

    小折腾:JavaScript与元素间的抛物线轨迹运动

    这回试试使用CSS实现抛物线运动效果

    相关文章

      网友评论

        本文标题:react hook封装购物车动画

        本文链接:https://www.haomeiwen.com/subject/wwcnihtx.html