react hook封装购物车动画

前阵子,开发过程中需要用到购物车动画,所以封装了动画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 (
      
{children}
); }, ); /** * 抛物线动画效果 * @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号球运动结束啦啦~~' });
      },
    },
    12,
  );
  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 (
    
); });

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封装购物车动画)