react hook封裝購物車動(dòng)畫

前陣子种樱,開發(fā)過程中需要用到購物車動(dòng)畫,所以封裝了動(dòng)畫hooks俊卤,在此做一下總結(jié)歸納嫩挤。

一、思考

首先消恍,購物車動(dòng)畫的軌跡是一個(gè)拋物線效果岂昭,這個(gè)我們可以通過CSS動(dòng)畫來實(shí)現(xiàn)。其次狠怨,我們的拋物線需要一個(gè)起始點(diǎn)约啊、一個(gè)目標(biāo)點(diǎn)邑遏、一個(gè)運(yùn)動(dòng)小球
然后恰矩,通過計(jì)算起始點(diǎn)和目標(biāo)點(diǎn)兩者之間 x 軸和 y 軸的距離记盒,然后通過 CSS 來改變運(yùn)動(dòng)小球的位置和移動(dòng)速度,從而實(shí)現(xiàn)加入購物車效果外傅。

思考框架.png

那么纪吮,這個(gè)拋物線動(dòng)畫效果如何實(shí)現(xiàn)?

高中物理告訴我們萎胰,當(dāng)物體運(yùn)動(dòng)時(shí)碾盟,X軸方向上和Y軸方向上的速度不一致時(shí),物體的運(yùn)動(dòng)效果就是拋物線技竟,類似我們向外拋球巷疼,小球的運(yùn)動(dòng)軌跡。

所以灵奖,想要有拋物線效果嚼沿,我們只需要控制運(yùn)動(dòng)小球,從起始點(diǎn)運(yùn)動(dòng)到目標(biāo)點(diǎn)的過程中瓷患,X軸和Y軸方向上的速度不一致即可骡尽。

因此,我們可以通過X軸方向上的速度不變擅编,通過Y軸方向上的速度變化攀细。

那么,如何控制Y軸上的速度變化爱态?

搜索前端 CSS 樣式谭贪,我們可以發(fā)現(xiàn),可以使用 transition-timing-function: linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(n,n,n,n);
屬性來實(shí)現(xiàn)過渡效果的速度變化锦担。

其中俭识,三階貝塞爾曲線cubic-bezier(x1, y1, x2, y2): 四個(gè)參數(shù)值分別在 0 到 1 之間,其中 (x1, y1)(x2, y2) 是控制曲線的變化程度洞渔。

快點(diǎn)擊鏈接套媚,去玩玩這個(gè)曲線吧!可好玩了磁椒!

什么是貝塞爾曲線堤瘤?快去了解它!浆熔!

二本辐、基本框架

我們思考一下,想要把這個(gè)動(dòng)畫效果封裝起來通用,我們需要傳入哪些必傳參數(shù)? 需要暴露哪些參數(shù)或方法給外層組件調(diào)用? 需要提供哪個(gè)參數(shù)便于個(gè)性化擴(kuò)展?

  1. 需要起始Dom節(jié)點(diǎn)慎皱、目標(biāo)Dom節(jié)點(diǎn)环葵;
  2. 需要暴露running方法,用于開啟動(dòng)畫效果宝冕;
  3. 需要運(yùn)動(dòng)小球张遭,小球包含兩層,外層flyOuter控制X軸勻速運(yùn)動(dòng)地梨,內(nèi)層flyInner控制Y軸變速運(yùn)動(dòng)菊卷;
  4. 需要提供屬性,支持自定義小球的內(nèi)容children宝剖、小球內(nèi)外層樣式 flyOuterStyle / flyInnerStyle 洁闰、小球運(yùn)動(dòng)時(shí)間設(shè)置runTime、小球開始運(yùn)動(dòng)回調(diào)beforeRun万细、小球開始運(yùn)動(dòng)回調(diào)afterRun扑眉;

hook封裝實(shí)現(xiàn)

import React, { useRef, useEffect, useImperativeHandle } from 'react';

import ReactDOM from 'react-dom';

/**
 * 動(dòng)畫球
 * @params children - 小球擴(kuò)展內(nèi)容
 * @params flyOuterStyle - 小球外層擴(kuò)展樣式
 * @params flyInnerStyle - 小球內(nèi)層擴(kuò)展樣式
 * @params runTime - 小球運(yùn)動(dòng)時(shí)間
 * @params ref - 小球dom實(shí)例
 */
const flyOuter = React.forwardRef(
  ({ children, flyOuterStyle = {}, flyInnerStyle = {}, runTime = 0.8 }, ref) => {
    const flyOuterRef = useRef();
    const flyInnerRef = useRef();
    useImperativeHandle(ref, () => ({ flyOuterRef, flyInnerRef }));


    // 運(yùn)動(dòng)小球外層樣式
    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,
    );

    // 運(yùn)動(dòng)小球內(nèi)層樣式
    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>
    );
  },
);


/**
 * 拋物線動(dòng)畫效果
 * @params startRef - 起始點(diǎn)dom節(jié)點(diǎn)
 * @params endRef - 目標(biāo)點(diǎn)dom節(jié)點(diǎn)
 * @params flyOuterStyle - 小球外層擴(kuò)展樣式
 * @params flyInnerStyle - 小球內(nèi)層擴(kuò)展樣式
 * @params runTime - 小球運(yùn)動(dòng)時(shí)間
 * @params beforeRun - 小球開始運(yùn)動(dòng)回調(diào)
 * @params afterRun - 小球結(jié)束運(yùn)動(dòng)回調(diào)
 * @params children - 小球擴(kuò)展內(nèi)容
 * @returns { running } - 小球開始運(yùn)動(dòng)函數(shù)
 */
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;

      // 現(xiàn)在起點(diǎn)距離終點(diǎn)的距離
      const startDot = startRef.current.getBoundingClientRect();
      const endDot = endRef.current.getBoundingClientRect();

      // 中心點(diǎn)的水平垂直距離
      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);

      // 頁面滾動(dòng)尺寸
      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`;

        // 開始動(dòng)畫
        flyOuterRef.style.transform = `translateX(${offsetX}px)`;
        flyInnerRef.style.transform = `translateY(${offsetY}px)`;

        // 動(dòng)畫標(biāo)志量
        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 };
}

三、測(cè)試用例

實(shí)現(xiàn)效果:


購物車動(dòng)畫.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: 購物車動(dòng)畫-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號(hào)球開始運(yùn)動(dòng)啦啦~~' });
      },
      afterRun: () => {
        notification.success({ message: '12號(hào)球運(yùn)動(dòng)結(jié)束啦啦~~' });
      },
    },
    <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}>
            發(fā)射中心
          </Button>
        </div>
      </div>

      <div className={styles.left}>
        <div ref={endRef_1}>
          <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-top']}>
            購物車1號(hào)
          </Button>
        </div>
        <div ref={endRef_2}>
          <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['left-bottom']}>
            購物車2號(hào)
          </Button>
        </div>
      </div>
      <div className={styles.right}>
        <div ref={endRef_3}>
          <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-top']}>
            購物車3號(hào)
          </Button>
        </div>
        <div ref={endRef_4}>
          <Button type="primary" icon={<ShoppingCartOutlined />} className={styles['right-bottom']}>
            購物車4號(hào)
          </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與元素間的拋物線軌跡運(yùn)動(dòng)

這回試試使用CSS實(shí)現(xiàn)拋物線運(yùn)動(dòng)效果

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末腰素,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子雪营,更是在濱河造成了極大的恐慌弓千,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件献起,死亡現(xiàn)場(chǎng)離奇詭異洋访,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谴餐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門姻政,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岂嗓,你說我怎么就攤上這事汁展。” “怎么了摄闸?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵善镰,是天一觀的道長(zhǎng)妹萨。 經(jīng)常有香客問我年枕,道長(zhǎng),這世上最難降的妖魔是什么乎完? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任熏兄,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摩桶。我一直安慰自己桥状,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布硝清。 她就那樣靜靜地躺著辅斟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪芦拿。 梳的紋絲不亂的頭發(fā)上士飒,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音蔗崎,去河邊找鬼酵幕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛缓苛,可吹牛的內(nèi)容都是我干的芳撒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼未桥,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼笔刹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起冬耿,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤徘熔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后淆党,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體酷师,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年染乌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了山孔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荷憋,死狀恐怖台颠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勒庄,我是刑警寧澤串前,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站实蔽,受9級(jí)特大地震影響荡碾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜局装,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一坛吁、第九天 我趴在偏房一處隱蔽的房頂上張望劳殖。 院中可真熱鬧,春花似錦拨脉、人聲如沸哆姻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽矛缨。三九已至,卻和暖如春帖旨,著一層夾襖步出監(jiān)牢的瞬間劳景,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工碉就, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盟广,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓瓮钥,卻偏偏與公主長(zhǎng)得像筋量,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碉熄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353