1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        React 輪播動畫探索

        共 14204字,需瀏覽 29分鐘

         ·

        2022-01-08 12:30

        1. 前言

        1.1. 氛圍氣泡需求

        最近投入了一個需求,遇到一個需要用動畫去實現(xiàn)的場景。

        我們的產(chǎn)品大大管它叫氛圍氣泡,在很多應(yīng)用(淘寶、抖音、bilibili)的直播間場景都會有類似這樣營造氛圍感的組件,能夠讓你感知到其他用戶在當(dāng)前直播間的行為。

        這個東西看起來轉(zhuǎn)瞬即逝的,但背后其實是基于一套和 push 通道相關(guān)的設(shè)計:

        前人栽樹后人乘涼,所幸大佬們把?push 消息中心?和?后臺服務(wù)?都建設(shè)得很完善,所以這次開發(fā)我只需要做這么一件事情:

        設(shè)置監(jiān)聽 push 的回調(diào),拿到數(shù)據(jù)渲染對應(yīng)組件。

        1.2. 開發(fā)評估

        看上去好簡單!讓我來簡單的評估一下它的開發(fā)成本:

        1.2.1. 首先看看它像啥(是否有現(xiàn)有組件可以復(fù)用)

        這東西一進(jìn)一出的,還撲棱撲棱的閃,好似一個輪播圖。在公共組件庫搜羅一下,找到了一個?Marquee(跑馬燈)?組件,它是基于?swiper/react?去實現(xiàn)的。

        swiper?大家都熟,一個功能非常強大且開箱即用的組件,目前已經(jīng)迭代到了 v7 版本。它也支持在現(xiàn)代前端框架下的使用,例如說支持?React。

        在 React 中,我們可以給它初始化一堆幻燈片,讓它可以滑動:

        1.2.2. swiper 實踐

        基礎(chǔ)示例

        import SwiperCore, { Autoplay } from"swiper";
        import { Swiper, SwiperSlide } from"swiper/react";

        // Import Swiper styles
        import"swiper/css";
        import"swiper/css/navigation";
        import"swiper/css/pagination";
        import"swiper/css/scrollbar";
        import"./styles.css";

        // install Swiper modules
        SwiperCore.use([Autoplay]);

        const renderBubble = (bubble) => {
        const { name, wording, btnText } = bubble;
        return (
        <SwiperSlide>
        <p className="live-bubble">
        <span className="live-bubble-username">{name}span>

        <span className="live-bubble-wording">{wording}span>
        <span className="live-bubble-action">{btnText}span>
        p>
        SwiperSlide>
        );
        };

        export default () => {
        const dataList = [
        { name: "李*", wording: "咨詢了課程", btnText: "去咨詢" },
        { name: "黃*", wording: "領(lǐng)取了優(yōu)惠券", btnText: "去領(lǐng)取" },
        { name: "高*", wording: "分享了直播間給好友", btnText: "去分享" },
        { name: "劉*", wording: "領(lǐng)取了直播間資料", btnText: "去領(lǐng)取" },
        { name: "朱*", wording: "購買了直播間課程《xxx》", btnText: "去領(lǐng)取" }
        ];

        return (
        <div style={{ background: "#000" }}>
        <Swiper
        slidesPerView={1}
        direction="horizontal"
        onSlideChange={() =>
        console.log("slide change")}
        autoplay
        >
        {dataList.map((item) => renderBubble(item))}
        Swiper>

        div>
        );
        };

        設(shè)置了 autoplay,可以自動播放,效果如下:

        細(xì)節(jié)改造

        你可能發(fā)現(xiàn)了,上面的示例跟想要實現(xiàn)的效果還差很遠(yuǎn),我們需要的效果是:

        • 輪播方向:從左至右

        • 進(jìn)入效果:從左到右一邊移入,一邊漸現(xiàn)

        • 退出效果:原地不動,漸隱

        接下來讓我們逐個擊破,改造一下 swiper。

        輪播方向修改

        autoplay 除了支持自動播放,還可以設(shè)置自動播放的方向。比如說,當(dāng)?direction?為?horizontal?的時候,每個滑塊默認(rèn)是向左退出和進(jìn)入的,即從右至左輪播。如果想要變成相反方向,可以這樣設(shè)置:

        autoplay={{
        delay: 3000,
        reverseDirection: true
        }}

        進(jìn)入效果和退出效果

        關(guān)于 swiper 切換效果的定制,官方也提供了一些切換效果相關(guān)的 api:effect。

        其中比較符合我們要求的應(yīng)該是?creativeEffect,創(chuàng)造性的切換效果。我們目前想要定制一套漸隱退出和滑動漸現(xiàn)進(jìn)入的效果。

        我們再設(shè)置一下 swiper 的參數(shù):

          slidesPerView={1}
        direction="horizontal"
        onSlideChange={() =>console.log("slide change")}
        autoplay={{
        delay: 3000,
        reverseDirection: true
        }}
        loop
        speed={3000}
        effect={"creative"}
        creativeEffect={{
        prev: {
        opacity: 0,
        translate: ['-300%', 0, 0]
        },
        next: {
        opacity: 0
        }
        }}
        >

        prev 和 next 的具體參數(shù)類型可以參考?swiper creativeEffect,比如說上面示例中?creativeEffect?的意思是:

        • 進(jìn)入動畫的起始狀態(tài)(prev):

          • translate:[’-300%‘, 0, 0],表示一開始在 x 軸的 -300% swiper 寬度的位置上

          • opacity:0,表示一開始透明不可見

        • 退出動畫的結(jié)束狀態(tài)(next):

          • opacity:0,表示結(jié)束時透明不可見

        經(jīng)過我們的改造,最終效果如下:

        局限性

        上面的效果其實并沒有完全滿足我們的要求,我們可以觀察到 swiper 的幻燈片進(jìn)入和退出有這樣的特點:會有某一段時間,上一張幻燈片和下一張幻燈片會同時存在于可視區(qū)域。

        這個會導(dǎo)致我們的滑動漸現(xiàn)進(jìn)入效果不能完美實現(xiàn),只能通過調(diào)整起始位置到盡可能遠(yuǎn),來擬合我們想要的效果。像上面其實就設(shè)置了 -300%,才達(dá)到了比較理想的效果。與之相對的,也帶來了另一個問題:透明度變化太快了,進(jìn)入可視區(qū)域時幻燈片的 opacity 已經(jīng)接近 1 了。

        這下可把我整不會了,沒想到 swiper 還有這樣的局限性。但幻燈片切換效果不佳并不是最主要的,更重要的還是氛圍氣泡業(yè)務(wù)邏輯的實現(xiàn),我們看看結(jié)合 push 命令,動態(tài)更新幻燈片數(shù)量的情況下,swiper 在 react 中的狀態(tài)管理會變得多不堪。

        另一個問題 —— 基于 swiper 動態(tài)更新氛圍氣泡

        在實際業(yè)務(wù)使用中,其實還遇到了優(yōu)先級展示的問題,氛圍氣泡的位置一共有三個組件在輪流展示:

        • 打開直播間,先展示一段 5s 的課程公告

        • 公告消失后,如果有氛圍氣泡數(shù)據(jù),就展示氛圍氣泡

        • 如果沒有氛圍氣泡,就展示兜底的引導(dǎo)進(jìn)群組件

        如果我們需要動態(tài)插入氛圍氣泡的話,就會有兩種情況:

        • 氛圍氣泡組件未初始化時:通過組件 state 去緩存氛圍氣泡數(shù)組

        • 氛圍氣泡組件初始化后:通過 swiper 實例,調(diào)用 swiper.appendSlide/prependSlide 方法去插入氛圍氣泡幻燈片

        比如說以下的業(yè)務(wù)代碼:

        // 氛圍氣泡推送監(jiān)聽
        onAtmosphereBubble((data) => {
        // 未展示前,緩存氣泡到狀態(tài)中
        if (this.state.chatBoxTopIndex === 0) {
        this.setState((prev) => ({
        bubbleList: [
        data,
        ...prev.bubbleList,
        ],
        }));
        } elseif (this.state.chatBoxTopIndex === 1) {
        // 展示中,通過 swiper 實例插入幻燈片
        this.addBubble(data);
        } else {
        // 即將展示,向狀態(tài)中插入幻燈片
        this.setState((prev) => ({
        bubbleList: [
        data,
        ...prev.bubbleList,
        ],
        }));
        this.nextChatBoxTop(1);
        }
        });

        const addBubble = (data) => {
        this.swiper?.prependSlide('
        new Slide
        '
        );
        };

        從這里就能看出來,在 react 中,如果需要動態(tài)更新幻燈片的場景,使用 swiper 相當(dāng)不合適。原因是 swiper 是通過示例方法去更新 UI,而 react 是通過 數(shù)據(jù)(狀態(tài))去更新 UI 的,兩者不太兼容。

        除此之外,實踐中也發(fā)現(xiàn)了很多其他的問題,比如說:

        • 通過 swiper.appendSlide/prependSlide 方法去插入新的幻燈片,只能傳入 HTML 字符串,不能傳入 React 組件。也就是說,新的幻燈片需要手動綁定事件,且不具備 React 的生命周期 hook。

        • swiper 的幻燈片數(shù)據(jù)由組件 state 和 swiper 實例共同控制,會出現(xiàn)兩者數(shù)據(jù)不同步的情況,不易理解和管理。

        1.3. 別的方案?

        總的來說,swiper 在這個需求里表現(xiàn)得不是很好,使用它反而會讓代碼變得復(fù)雜。既然沒有現(xiàn)有的組件可以復(fù)用,我們可以怎么另辟蹊徑呢?接下來就來到本文的正題了,我們來通過一個神奇的 React 動畫庫來實現(xiàn)我們的需求。

        2. react-transition-group

        react-transition-group?是 React 官方實現(xiàn)的,用于操作過渡效果的組件庫。它可以在組件安裝和卸載時,增加過渡效果。一共提供了 4 個 api,上手成本極低。

        我們首先從單個組件切換的場景入手,比如說現(xiàn)在希望為一個組件增加進(jìn)入和退出的動畫,就可以使用 Transition 或者 CSSTransition。

        2.1. Transition 組件

        Transition 組件是一個自由度比較高的組件,CSSTransition 也是基于它擴展的。它通過 in 參數(shù)跟蹤組件的進(jìn)入和退出(或者說顯隱),并由開發(fā)者自定義動畫樣式。

        話不多說,我們直接上代碼。下面設(shè)計了一個按鈕點擊來控制組件進(jìn)入退出的示例:

        • index.js

        import React, { useState } from"react";
        import { Transition } from"react-transition-group";

        import"./styles.css";

        const duration = 500;

        const defaultStyle = {
        transition: `opacity ${duration}ms ease-in-out`,
        opacity: 0
        };

        const transitionStyles = {
        entering: { opacity: 1 },
        entered: { opacity: 1 },
        exiting: { opacity: 0 },
        exited: { opacity: 0 }
        };

        exportdefaultfunction App() {
        const [inProp, setInProp] = useState(false);
        return (
        <div>
        <Transition in={inProp} timeout={duration}>
        {(state) => (
        <div
        style={{
        ...defaultStyle,
        ...transitionStyles[state]
        }}
        >

        I'm a fade Transition!
        div>

        )}
        Transition>
        <button onClick={() => setInProp((prev) => !prev)}>Click to Enterbutton>
        div>
        );
        }

        效果如下:

        Transition 包含了以下參數(shù):

        • in:控制組件顯示的布爾值,觸發(fā)進(jìn)入或退出狀態(tài)

        • timeout:動畫的持續(xù)時間,單位為毫秒,可以一次設(shè)置所有狀態(tài)的動畫時間,也可以單獨設(shè)置每個狀態(tài)的動畫時間。這個時間主要是結(jié)合 SwitchTransition api 使用的,需要和 css 中的動畫時間保持一致。

        timeout={500}

        // or

        timeout={{
        appear: 500,
        enter: 300,
        exit: 500,
        }}
        • children 函數(shù):提供了一個 state 參數(shù)的 children 函數(shù),用于渲染我們的組件。其中 state 包括了以下狀態(tài):

          • 'entering'

          • 'entered'

          • 'exiting'

          • 'exited'

        • 開始動畫的三個鉤子,均接收一個回調(diào)函數(shù)?Function(node: HtmlElement, isAppearing: bool) -> void?,回調(diào)函數(shù)接收 2 個參數(shù),第一個參數(shù)為當(dāng)前元素的 dom 節(jié)點,第二個參數(shù)表示當(dāng)前動畫是否為元素初次掛載時發(fā)生

          • onEnter:在動畫狀態(tài)變?yōu)?entering 之前調(diào)用

          • onEntering:在動畫狀態(tài)變?yōu)?entering 之后調(diào)用

          • onEntered:在動畫狀態(tài)變?yōu)?entered 之后調(diào)用

        • 離開動畫的三個鉤子,均接收一個回調(diào)函數(shù)?Function(node: HtmlElement) -> void?,回調(diào)函數(shù)僅接收當(dāng)前元素的 dom 節(jié)點

          • onExit:在動畫狀態(tài)變?yōu)?exiting 之前調(diào)用

          • onExiting:在動畫狀態(tài)變?yōu)?exiting 之后調(diào)用

          • onExited:在動畫狀態(tài)變?yōu)?exited 之后調(diào)用

        • ......

        2.2. CSSTransition 組件

        通過上面 Transition 的例子,我們也看到了,組件當(dāng)前的 class 是由 children 函數(shù)中的 state 參數(shù)來決定的,寫法上不夠 auto。CSSTransition 組件解決了這一問題,它繼承了 Transition 組件,并簡化了 className 的聲明。

        同理,我們上一段代碼看看區(qū)別,依然是剛才的例子:

        • index.js

        import React, { useState } from"react";
        import { CSSTransition } from"react-transition-group";

        import"./styles.scss";

        const duration = 300;

        exportdefaultfunction App() {
        const [inProp, setInProp] = useState(false);
        return (
        <div>
        <CSSTransition in={inProp} timeout={duration} classNames="my-fade">
        <p className="my-fade">I'm a fade Transition!p>

        CSSTransition>
        <button onClick={() => setInProp((prev) => !prev)}>Click to Enterbutton>
        div>
        );
        }
        • style.scss

        .my-fade {
        ?opacity: 0;
        ?&-enter { opacity: 0 }
        ?&-enter-active {
        ? ?opacity: 1;
        ? ?transition: opacity 300ms;
        ?}
        ?&-enter-done {
        ? ?opacity: 1;
        ?}
        ?&-exit { opacity: 1 }
        ?&-exit-active {
        ? ?opacity: 0;
        ? ?transition: opacity 300ms;
        ?}
        ?&-exit-done {
        ? ?opacity: 0;
        ?}
        };

        效果依然是:

        CSSTransition 會根據(jù) in 參數(shù)的變化,為組件添加不同的 class。例如,當(dāng) in 變?yōu)?true,會先后為組件添加 {classNames}-enter、{classNames}-enter-active、{classNames}-enter-done 的 class,形成入場的動畫效果;當(dāng) in 變?yōu)?false,則會為組件添加 {classNames}-exit、{classNames}-exit-active、{classNames}-exit-done 的 class,形成退場的動畫效果。

        CSSTransition 默認(rèn)有三個階段 —— 消失(appear)、進(jìn)入(enter)、離開(exit)。并且每個階段都先后添加三個類名,以 enter 為例,分別是:

        • enter 表示開始動畫的初始階段

        • enter-active 表示開始動畫的激活階段

        • enter-done 表示開始動畫的結(jié)束階段,也是樣式的持久化展示階段

        CSSTransition 的參數(shù)跟 Transition 差別不大,需要注意的是?classNames?這個參數(shù)。為了與 React 中的 className 進(jìn)行區(qū)別,classNames?這個參數(shù)在?className?的基礎(chǔ)上在末尾加了個?s。

        一般來說,這個參數(shù)將作為動畫過渡的一系列類名(-enter、-enter-active、-enter-done、......)的前綴。

        classNames="my-fade"

        // my-fade-enter
        // my-fade-enter-active
        // my-fade-enter-done
        // ......

        但也支持對每個類名單獨定義:

        classNames={{
        appear: 'my-appear',
        appearActive: 'my-active-appear',
        appearDone: 'my-done-appear',
        enter: 'my-enter',
        enterActive: 'my-active-enter',
        enterDone: 'my-done-enter',
        exit: 'my-exit',
        exitActive: 'my-active-exit',
        exitDone: 'my-done-exit',
        }}

        2.3. SwitchTransition

        SwitchTransition 用于包裹 Transition 或 CSSTransition 組件,并修改它們內(nèi)部組件的過渡模式。它擁有一個?mode?參數(shù),可以實現(xiàn)兩種效果:

        • out-in?:當(dāng)前元素先轉(zhuǎn)出,然后當(dāng)完成時,新元素轉(zhuǎn)入。

        • in-out?:新元素首先轉(zhuǎn)入,然后當(dāng)完成時,當(dāng)前元素轉(zhuǎn)出。

        默認(rèn):?'out-in'

        同樣上代碼來看看效果:

        • index.js

        import React, { useState } from"react";
        import { CSSTransition, SwitchTransition } from"react-transition-group";
        import { Button, Form } from"react-bootstrap";

        import"./styles.scss";

        const modes = ["out-in", "in-out"];

        exportdefaultfunction App() {
        ?const [mode, setMode] = useState("out-in");
        ?const [state, setState] = useState(true);
        ?return (
        ? ?<div>
        ? ? ?<div className="label">Mode:div>

        ? ? ?<div className="modes">
        ? ? ? ?{modes.map((m) => (
        ? ? ? ? ?<Form.Check
        ? ? ? ? ? ?key={m}
        ? ? ? ? ? ?custom
        ? ? ? ? ? ?inline
        ? ? ? ? ? ?label={m}
        ? ? ? ? ? ?id={`mode=msContentScript${m}`}
        ? ? ? ? ? ?type="radio"
        ? ? ? ? ? ?name="mode"
        ? ? ? ? ? ?checked={mode === m}
        ? ? ? ? ? ?value={m}
        ? ? ? ? ? ?onChange={(event) =>
        {
        ? ? ? ? ? ? ?setMode(event.target.value);
        ? ? ? ? ? ?}}
        ? ? ? ? ?/>
        ? ? ? ?))}
        ? ? ?div>
        ? ? ?<div className="main">
        ? ? ? ?<SwitchTransition mode={mode}>
        ? ? ? ? ?<CSSTransition
        ? ? ? ? ? ?key={state}
        ? ? ? ? ? ?addEndListener={(node, done) =>
        {
        ? ? ? ? ? ? ?node.addEventListener("transitionend", done, false);
        ? ? ? ? ? ?}}
        ? ? ? ? ? ?classNames="fade"
        ? ? ? ? ?>
        ? ? ? ? ? ?<div className="button-container">
        ? ? ? ? ? ? ?<Button onClick={() => setState((state) => !state)}>
        ? ? ? ? ? ? ? ?{state ? "Hello, world!" : "Goodbye, world!"}
        ? ? ? ? ? ? ?Button>
        ? ? ? ? ? ?div>
        ? ? ? ? ?CSSTransition>
        ? ? ? ?SwitchTransition>
        ? ? ?div>
        ? ?div>
        ?);
        }
        • style.scss

        body {
        ?padding: 2rem;
        ?font-family: sans-serif;
        ?text-align: center;
        }

        .label {
        ?margin-bottom: 0.5rem;
        }

        .modes {
        ?margin-bottom: 1rem;
        }

        .button-container {
        ?margin-bottom: 0.5rem;
        }

        .fade {
        ?&-enter .btn {
        ? ?opacity: 0;
        ? ?transform: translateX(-100%);
        ?}
        ?&-enter-active .btn {
        ? ?opacity: 1;
        ? ?transform: translateX(0%);
        ?}
        ?&-exit .btn {
        ? ?opacity: 1;
        ? ?transform: translateX(0%);
        ?}
        ?&-exit-active .btn {
        ? ?opacity: 0;
        ? ?transform: translateX(100%);
        ?}
        ?&-enter-active .btn,
        ?&-exit-active .btn {
        ? ?transition: opacity 500ms, transform 500ms;
        ?}
        }

        效果如下:

        從這里可以看出和 swiper 的區(qū)別,swiper 類似于?in-out?的效果,而我們希望實現(xiàn)的氛圍氣泡是?out-in?的效果。

        2.4. TransitionGroup

        顧名思義,TransitionGroup?是用來管理多個 Transition/CSSTransition 的。當(dāng)需要管理多個 Transition,即需要實現(xiàn)不同的動畫效果時,適合使用它。


        ?{list.map(({ id }) => (
        ? ?<CSSTransition
        ? ? ?key={id}
        ? ? ?timeout={300}
        ? ? ?classNames="my"
        ? ?>

        ? ? ?<MyComponent key={id}/>
        ? ?CSSTransition>

        ?))}

        3. 用 react-transition-group 實現(xiàn)氛圍氣泡

        了解了?react-transition-group?之后,我們很容易聯(lián)想到,可以用 CSSTransition + SwitchTransition(out-in mode) 實現(xiàn)我們的需求。

        但在實現(xiàn)之前,還需要定義一下我們的數(shù)據(jù)結(jié)構(gòu)。

        3.1. 數(shù)據(jù)結(jié)構(gòu)

        氣泡是隨著用戶的交互,從客戶端觸發(fā)上報給到后臺,再由后臺反饋到其他客戶端的。由于上文提到氛圍氣泡不是常駐的,會去展示其他的組件,所以當(dāng)后臺反饋了新的氣泡數(shù)據(jù),會存在三種情況:

        • 正在展示氛圍氣泡:將氣泡數(shù)據(jù)插入到展示順序的尾部。

        • 正在展示不可中斷的組件(課程公告):將氣泡數(shù)據(jù)緩存起來。

        • 正在展示可中斷的組件(兜底組件):將氣泡數(shù)據(jù)緩存起來,并立即展示氛圍氣泡。

        顯然是一個?隊列,我們可以維護(hù)一個氣泡的 JSX 元素數(shù)組,用一個 index 來決定當(dāng)前展示的氣泡來實現(xiàn)切換渲染,并將不斷到來的氣泡數(shù)據(jù)插入到數(shù)組的尾部。

        這樣的好處在于,相比 swiper/react 通過狀態(tài)和實例來維護(hù)氣泡的方式,我們統(tǒng)一使用狀態(tài)來維護(hù)氣泡數(shù)據(jù)更加符合數(shù)據(jù)驅(qū)動視圖的思想。

        3.2. 隊列實現(xiàn)

        我們將氣泡列表的展示順序(index)放到 props 中維護(hù),使之變成受控的。并在隊列中維護(hù)一個定時器,定時觸發(fā) props 中的 nextBubble 方法去更新 index。如下:

        import React, { useEffect, useCallback } from"react";
        import { CSSTransition, SwitchTransition } from"react-transition-group";
        import"./index.css";

        const AtmosphereBubbleSequence = ({
        ?bubbleList,
        ?index,
        ?setCurIndex,
        ?nextBubble,
        ?next,
        ?resetList
        }
        ) =>
        {
        ?const bubbleListLength = bubbleList.length;
        ?let curIndex = 0;

        ?if (index > -1 && index < bubbleListLength) {
        ? ?curIndex = index;
        ?} else {
        ? ?curIndex = bubbleListLength - 1;
        ? ?setCurIndex(curIndex);
        ?}

        ?useEffect(() => {
        ? ?// 定時觸發(fā)
        ? ?const interval = setInterval(() => {
        ? ? ?nextBubble();
        ? ?}, 3000);
        ? ?return() => {
        ? ? ?clearInterval(interval);
        ? ?};
        ?}, []);

        ?const onExited = useCallback(
        ? ?(node) => {
        ? ? ?if (+node.dataset.bubbleKey === bubbleListLength - 1) {
        ? ? ? ?// 切換到兜底組件
        ? ? ? ?next();
        ? ? ? ?// 清空氣泡列表
        ? ? ? ?resetList();
        ? ? ?}
        ? ?},
        ? ?[bubbleListLength, next, resetList]
        ?);

        ?return (
        ? ?<SwitchTransition>
        ? ? ?<CSSTransition
        ? ? ? ?key={bubbleList[index]?.type?.name + index || Math.random()}
        ? ? ? ?timeout={{
        ? ? ? ? ?enter: 300,
        ? ? ? ? ?exit: 300
        ? ? ? ?}}
        ? ? ? ?classNames="atmosphere-bubble-cnt"
        ? ? ? ?unmountOnExit
        ? ? ? ?onExited={onExited}
        ? ? ?>

        ? ? ? ?<div data-bubble-key={index} className="atmosphere-bubble-cnt">
        ? ? ? ? ?{bubbleList[index]}
        ? ? ? ?div>

        ? ? ?CSSTransition>
        ? ?SwitchTransition>
        ?);
        };

        export default AtmosphereBubbleSequence;

        值得注意的是,需要在 onExited 回調(diào)中去判斷氣泡列表是否已經(jīng)展示完畢,調(diào)用銷毀氣泡序列組件的方法,并清空氣泡數(shù)據(jù)列表,去展示其他的組件。

        同時,我們設(shè)置氣泡的樣式如下:

        .atmosphere-bubble-cnt {
        ?padding: 016px;
        }

        .atmosphere-bubble-cnt-enter {
        ?opacity: 0;
        ?transform: translate3d(-60px, 0, 0);
        }

        .atmosphere-bubble-cnt-enter-active {
        ?opacity: 1;
        ?transform: translate3d(0, 0, 0);
        ?transition: opacity 300ms, transform 300ms;
        }

        .atmosphere-bubble-cnt-exit {
        ?opacity: 1;
        }

        .atmosphere-bubble-cnt-exit-active {
        ?opacity: 0;
        ?transition: opacity 300ms;
        }

        3.3. 效果模擬

        為了試驗效果,我們在外部設(shè)置一段 mock 邏輯,來模擬不斷塞入氣泡數(shù)據(jù)的情況:

        import React, { useEffect, useState } from"react";
        import Bubble from"./Bubble";
        import AtmosphereBubbleSequence from"./Sequence";
        import"./styles.css";

        exportdefaultfunction App() {
        ?const [index, setIndex] = useState(0);

        ?const [bubbleDataList, setBubbleDataList] = useState([
        ? ?{ nick: "李*", content: "咨詢了課程", eventMsg: "去咨詢" },
        ? ?{ nick: "黃*", content: "領(lǐng)取了優(yōu)惠券", eventMsg: "去領(lǐng)取" },
        ? ?{ nick: "高*", content: "分享了直播間給好友", eventMsg: "去分享" },
        ? ?{ nick: "劉*", content: "領(lǐng)取了直播間資料", eventMsg: "去領(lǐng)取" },
        ? ?{ nick: "朱*", content: "購買了直播間課程《xxx》", eventMsg: "去領(lǐng)取" }
        ?]);

        ?useEffect(() => {
        ? ?const interval = setInterval(() => {
        ? ? ?setBubbleDataList((prev) => [
        ? ? ? ?...prev,
        ? ? ? ?{ nick: "朱*", content: "購買了直播間課程《xxx》", eventMsg: "去領(lǐng)取" }
        ? ? ?]);
        ? ?}, 3000);
        ? ?return() => {
        ? ? ?clearInterval(interval);
        ? ?};
        ?}, []);

        ?if (!bubbleDataList.length) {
        ? ?returnnull;
        ?}

        ?const nextBubble = (newIndex) => {
        ? ?setIndex((prevIndex) => {
        ? ? ?returntypeof newIndex === "undefined" ? prevIndex + 1 : newIndex;
        ? ?});
        ?};

        ?return (
        ? ?<div
        ? ? ?style={{
        ? ? ? ?background: "#000",
        ? ? ? ?height: "56px",
        ? ? ? ?display: "flex",
        ? ? ? ?alignItems: "center"
        ? ? ?}}
        ? ?>

        ? ? ?<AtmosphereBubbleSequence
        ? ? ? ?bubbleList={bubbleDataList.map((data) =>
        (
        ? ? ? ? ?<Bubble data={data} />
        ? ? ? ?))}
        ? ? ? ?index={index}
        ? ? ? ?setCurIndex={setIndex}
        ? ? ? ?nextBubble={nextBubble}
        ? ? ?/>
        ? ?div>

        ?);
        }

        接著來看看效果:

        很好,已經(jīng)實現(xiàn)我們想要的效果了!

        4. 總結(jié)

        在本文我們接觸到了 swiper 和 react-transition-group 的使用,并分別用它們實現(xiàn)了氛圍氣泡需求。

        4.1. 動畫效果層面的對比

        • react-transition-group 更加靈活,針對組件過渡的動畫效果有更廣泛的應(yīng)用場景。

        • swiper 作為輪播效果組件,它受限于前后幻燈片同時存在的這一問題,在氛圍氣泡需求中表現(xiàn)不是很好。

        4.2. 狀態(tài)管理層面的對比

        雖然 swiper 有這樣的局限性,但這一問題并不是不能解決的,還是有 hack 技巧的。比如說,可以先把 swiper-container 先漸隱,再觸發(fā)幻燈片切換,并在中途增加動畫類實現(xiàn)漸現(xiàn)。只是說這段 js 邏輯不太優(yōu)雅而已:

        componentDidMount() {
        ?const { next, setBubbleList } = this.props;
        ?const swiperIns = this.swiper;
        ?const interval = setInterval(() => {
        ? ?swiperIns.$wrapperEl?.addClass('swiperFadeOut');
        ? ?delay(300)
        ? ? ?.then(() => {
        ? ? ? ?if (swiperIns.activeIndex === 0) {
        ? ? ? ? ?clearInterval(interval);
        ? ? ? ? ?next(2);
        ? ? ? ? ?setBubbleList();
        ? ? ? ? ?thrownewError('退出動畫');
        ? ? ? ?}
        ? ? ? ?// 設(shè)置上一個滑塊為透明隱藏
        ? ? ? ?(swiperIns.slides[swiperIns.activeIndex] as HTMLElement).style.opacity = '0';
        ? ? ? ?swiperIns.slidePrev(500);
        ? ? ? ?return delay(300);
        ? ? ?})
        ? ? ?.then(() => {
        ? ? ? ?swiperIns.$wrapperEl.addClass('swiperFadeIn');
        ? ? ? ?return delay(200);
        ? ? ?})
        ? ? ?.then(() => {
        ? ? ? ?swiperIns.$wrapperEl.removeClass('swiperFadeOut');
        ? ? ? ?swiperIns.$wrapperEl.removeClass('swiperFadeIn');
        ? ? ?})
        ? ? ?.catch((error) => {
        ? ? ? ?if (error.message === '退出動畫') {
        ? ? ? ? ?console.log('已退出');
        ? ? ? ?}
        ? ? ?});
        ?}, 2000);
        }

        由此也可以發(fā)現(xiàn),在狀態(tài)管理方面:

        • react-transition-group 可以讓我們大膽地設(shè)計數(shù)據(jù)結(jié)構(gòu),來管理組件的過渡狀態(tài)。

        • swiper 相對不太適合 react 的狀態(tài)管理,在需要動態(tài)增刪幻燈片的場景,它依賴于實例方法,不易做到數(shù)據(jù)同步。

        4.3. 方案選擇

        面對類似氛圍氣泡的需求,如何選擇 swiper 和 react-transition-group 這兩類實現(xiàn)方案?

        其實只要觀察,數(shù)據(jù)列表的長度是靜態(tài)的,還是會動態(tài)改變的。

        • 靜態(tài):使用幻燈片組件,如 swiper

        • 動態(tài):使用 react 生態(tài)的組件,如 react-transition-group

        其中原因,相信你已經(jīng)有所理解~

        5. CodeSandbox demo




        緊追技術(shù)前沿,深挖專業(yè)領(lǐng)域
        掃碼關(guān)注我們吧!
        瀏覽 39
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            大逼网| 奇米影视成人在线 | 日韩精品一区二区三区四虎影视 | 手机超碰在线 | 综合无码AV在线 | 大香蕉在线精品 | 日本欧美在线视频 | 欧美老妇激情视频 | 黄色大片网站 | 欧美性爱影音先锋 |