「React」如何在React中優雅的實現動畫

張越發表於2020-10-09

最簡單的動畫元件實現

動畫的本質,無非就是一個狀態樣式到另一個狀態樣式的過渡。最簡單的動畫元件,我們只需要指定兩個狀態的樣式(進入的樣式,離開的樣式),以及一個開關(控制狀態),即可完成。

animate.png

codepen地址

animate.gif

實現一組動畫的過渡

實現一組動畫的過渡。我們只需要在多個最簡單的動畫元件的基礎之上,設定一個統一的開關,統一控制,多個動畫元件動畫的狀態即可。如果想實現有交錯的過渡(有時間間隔的過渡),我們只需要根據動畫元件在一組元素中的索引位置,設定合適的延遲即可。

為了引入統一的開關的控制,我們為動畫元件新增一個父級元件,父級元件的開關控制所有子元件的開關狀態。父元件使用React.Context將自己開關的狀態,下發給子元件。

為了實現交錯效果,我們需要為列表中子元件設定不同長度的延遲。延遲時長和子元件在列表索引,以及開關的狀態有關。

比如:

在開關設定為true, 需要顯示入場的動畫。延遲自上而下,依次增大(0ms, 100ms, 200ms, 300ms)

在開關設定為false, 需要顯示出場的動畫。延遲自上而下,依次減小(300ms, 200ms, 100ms, 0ms)

animates1.png

animates2.png

codepen地址

animates.gif

上述元件目前存在的問題

  1. 節點必須事先已經渲染好,對於動態插入的節點,這些動畫元件無能為力。(在下方我們參考了`react-transition-group實現解決這個問題)
  2. 如果元素起始樣式有display: none動畫將不會起效果(這個問題其實和動態插入節點屬於一類問題)。
  3. 對於一組列表節點。新的節點的插入,和刪除時。其他節點的過渡很生硬,沒有動畫效果(我們可以使用flip動畫解決這個問題)。

FLIP動畫

FLIP動畫實現原理是: 快取元素起點的位置, 然後將元素置於終點的位置,計算終點與起點的差值,根據差值應用動畫。

我們先看看flip動畫強大的效果

效果1.gif

效果2.gif

接下來,我們來一步一步實現一個簡易的flip動畫,然後再嘗試在react中實現。

閃爍

請問下面的程式碼,會造成閃爍的問題嗎?

flip1.png

codepen演示

答案: 是不會。具體原因和瀏覽器的事件佇列有關。點選事件的程式碼,我們必須執行完成當前的任務(當前程式碼段的執行)才會進行瀏覽器渲染。

這一點對我們很重要。再重申一遍flip動畫的原理,快取元素起點的位置, 然後將元素置於終點的位置,計算終點與起點的差值,根據差值應用動畫。

雛形

然後基於上面的程式碼,我們目前可以實現一個簡易的flip動畫

flip2.png

codepen演示

我們可以看到,動畫已經實現,但是目前動畫的計算還是固定的,我們接下來嘗試讓它自動化。

fli3.gif

完善

我們嘗試對, 之前狀態和當前狀態的屬性,做自動的差值計算。

flip3.png

codepen演示

我們目前已經實現了,寬度和x軸的flip動畫。

flip4.gif

?️為什麼要這樣計算(之前的位置 - 現在的位置)?

我們為什麼要使用之前的樣式值減去當前的樣式值?

FLIP動畫的原理是基於當前位置和起始位置的動畫,我們在做動畫的時候,元素其實已經到達了結束的位置。

比如當前的位置是100px, 開始位置是0px。flip動畫需要模擬從0px到100px的過程,但是當前位置已經是100px了,所以我們必須使用 translateX(0 - 100px), 模擬動畫開始時的0px的位置。

100ms

There is a window of 100ms after someone interacts with your site where you’re able to do work without them noticing.

使用flip動畫時,切記計算不能超過100ms,如果超過100ms使用者會感到卡頓。

100ms.jpg

FLIP 與 Web Animations API

目前距離實現一個真正的flip動畫庫還有不少的距離。繼續使用 requestAnimationFrame 會很困難,太複雜了。

既然flip動畫,是基於結束位置和開始位置的動畫,那麼有沒有什麼好辦法,不需要我們手動的去調整。只需要提供初始位置和結束位置完成動畫呢?我們可以使用Web Animations API。

對於 Web Animations API本身,我在這裡不想做過多的介紹。大家只需要知道,使用Web Animations API後,我們只需要設定開始的樣式,和結束的樣式,動畫就會自動完成。

我們將上面的demo,改造成使用Web Animations API的形式。

flip5.png

codepen演示

flip6.gif

可以看到,我們在程式碼裡只需要設定開始和結束的樣式,動畫就會自動過渡完成。

React 與 FLIP

如何在react中完成flip動畫呢?我們首先回憶下在js中flip動畫的邏輯

  1. 快取元素起始位置
  2. 將元素移動到結束的位置
  3. 獲取當前的位置,並計算當前的位置與快取的起始位置的差值。
  4. 下一幀開始時,開始做動畫

我們可以發現,第1,2,3步都是發生在渲染到螢幕之前(或者說渲染到螢幕的那一刻)。那麼在react中,有什麼hook發生在渲染到頁面的那一刻呢?答案是: 函式元件中是useLayoutEffect。class元件中是componentDidUpdate

我們整理下在react中flip動畫的實現邏輯

  1. 在頁面第一次useEffect, 元素渲染完成。這時同時快取元素的位置。
  2. state發生變化,元件需要重新渲染
  3. 在元件重新渲染到螢幕那一刻,在useLayoutEffect中,我們獲取最新的位置。並計算當前的位置與快取的起始位置的差值。
  4. 動畫開始執行

那麼接下來我們來實現一個react中flip的雛形

flip7.png

codepen演示

flip8.gif

bigo!我們成功在react中實現了flip動畫

❓ 目前存在的問題

如果我們在flip動畫執行過程中,切換動畫。動畫會出現閃爍,我們現在來著手解決這個問題。

我們先來思考一下這個問題產生的原因。動畫在執行過程中,還沒有到達終點,這時切換動畫,動畫元素會被強行移動到終點的位置,然後進行下一次動畫,這就是動畫閃爍的原因。

如何解決呢?

  1. 在切換動畫的時候,如果上一次動畫沒有結束,我們手動將其結束
  2. 在切換動畫的時候,更新位置的快取。

flip8.png

codepen演示

flip9.gif

雖然目前已經實現flip動畫的效果,但是距離封裝成可用的庫還有些距離,如果大家想要了解的更多,可以檢視我封裝好的原始碼(原理和上面的文章是一模一樣的)。倉庫地址: https://github.com/peoplesing...

? Flip動畫需要注意的點

  1. flip計算動畫位置時,元素上最好不要有transition的css屬性,會影響到位置的計算。
  2. 之前的計算快取位置時,都是相對於body的位置。但是如果存在有滾動條時,快取的位置會有問題。解決辦法是,基於動畫元素的父級元素計算位置,而不是body的位置。

Flip如何實現交錯效果?

好吧。目前我的庫中,交錯效果的完善解決方案還沒有實現。但是主體思路是有了,並有了簡易的實現版本 見下方?

效果3.gif

??? 動態插入節點的動畫處理

這個問題解決的思路,我參考了 react-transition-group庫 的原始碼。在這裡我說一下,react-transition-group庫 實現的思路。


<react-transition-group>
  {
    list && list.map((item) => (
      <react-transition>
        { item }
      </react-transition>
    ))
  }
<react-transition-group>

最外層的 <react-transition-group> 元件 並不會直接對嵌入的children進行直接渲染。而是將props.children儲存為,元件的內部狀態state。這樣我們可以在children渲染之前,對state做一些額外的操作。

<react-transition-group>會對於動態插入的節點,不會直接渲染。而是先將,新插入節點外層的<react-transition>元件的動畫狀態設定為'Leave'態(這裡處理的目的是,即使dom渲染完成後,元素也是隱藏的狀態)。然後在<react-transition>中,會先等待dom渲染完成,然後再將動畫的狀態設定為'Entering',完成'Leave'態到'Entering'態的動畫過渡。

<react-transition-group>會對於動態刪除的節點,不會直接刪除。而是先將需要刪除節點外層的<react-transition>元件的動畫開關設定為false,動畫開始向'Leave'態過渡。動畫過渡完成後,然後會觸發<react-transition>元件的 onLeave 事件。在 onLeave 事件中會刪除dom節點。

總結一下 react-transition-group 庫的處理方式:

  1. 插入的節點,先渲染dom,然後再做動畫
  2. 刪除的節點,先做動畫,然後再刪除dom

寫在最後

如果您對我的文章感到滿意,還請麻煩您給我的文章點一個贊。如果您喜歡我的小專案,還請幫我的小專案點一個star。謝謝?

專案地址:https://github.com/peoplesing...

參考

相關文章