讓動畫變得更簡單之FLIP技術

清夜發表於2019-01-31

某次被問到如何實現以下動畫效果:

讓動畫變得更簡單之FLIP技術

若干個元素卡片從上而下排列,當增加或刪除某個卡片的時候,其餘的卡片會以一種 transition動畫的形式移動到適當的位置上,而不是生硬地閃現

當時我恰好看過 Vue中的內建元件 transition的實現,意識到完全可以用 transition元件的部分原理來完成這個效果,但是由於沒有深入地探究過為什麼是這樣,只停留在表面,知其然而不知其所以然,所以儘管我知道如何實現這個效果,但很難解釋為什麼是這樣,語言組織地比較困難

後來我無意間看到一篇文章 FLIP技術給Web佈局帶來的變化,立馬恍然大悟,原來這個東西叫 FLIP

FLIP

FLIPFirstLastInvertPlay四個單詞首字母的縮寫

First,指的是在任何事情發生之前(過渡之前),記錄當前元素的位置和尺寸,即動畫開始之前那一刻元素的位置和尺寸資訊,可以使用 getBoundingClientRect()這個 API來處理(大部分情況下其實 offsetLeftoffsetTop也是可以的)

Last:執行一段程式碼,讓元素髮生相應的變化,並記錄元素在動畫最後狀態的位置和尺寸,即動畫結束之後那一刻元素的位置和尺寸資訊

Invert:計算元素第一個位置(First)和最後一個位置(Last)之間的位置變化(如果需要,還可以計算兩個狀態之間的尺寸大小的變化),然後使用這些數字做一定的計算,讓元素進行移動(通過 transform來改變元素的位置和尺寸),從而建立它位於第一個位置(初始位置)的一個錯覺

即,一上來直接讓元素處於動畫的結束狀態,然後使用 transform屬性將元素反轉回動畫的開始狀態(這個狀態的資訊在 First步驟就拿到了)

Play:將元素反轉(假裝在first位置),我們可以把 transform設定為 none,因為失去了 transform的約束,所以元素肯定會往本該在的位置(即動畫結束時的那個狀態)進行移動,也就是last的位置,如果給元素加上 transition的屬性,那麼這個過程自然也就是以一種動畫的形式發生了

按照我的理解,就是對動畫元素起止狀態的一個量化,量化成一個公式,絕大部分的連續動畫都可以通過套用這個公式來完成,提升動畫的開發效率,更加詳細的請自行參見 FLIP技術給Web佈局帶來的變化

實現卡片 Card增刪動畫

瞭解了 FLIP這個概念之後,再來實現開頭提到的那個動畫效果,其實就很簡單了

First

記錄在動畫開始之前每個卡片的位置和尺寸資訊,這裡因為卡片的尺寸在動畫過程中其實是不會發生任何變化的, 所以可以略過這一步,只記錄卡片的位置資訊

另外,如果所有卡片的尺寸都是相同的,那麼也無需記錄所有卡片的位置資訊,因為無論是插入卡片還是刪除卡片,都只有那些位於位置座標在變化卡片座標的後面的卡片才會受到影響的,前面的是不會變的

// First
activeList.forEach((itemEle, index) => {
  rectInfo = itemEle.getBoundingClientRect()
  transArr[index + stepIndex][0] = rectInfo.left
  transArr[index + stepIndex][1] = rectInfo.top
})
複製程式碼

Last

動畫的結束狀態,其實就是增加或者刪除了卡片之後,其餘卡片的狀態:

if (updateStatus === 0) {
  // 增加卡片
  newListData = this.state.listData.slice(0, activeIndex).concat({
    index: cardIndex++
  }, this.state.listData.slice(activeIndex))
} else {
  // 刪除卡片
  newListData = this.state.listData.filter((value, index) => index !== activeIndex)
}
複製程式碼

因為這個時候沒給卡片加 transition屬性,所以卡片數量更新這個過程,其實就是一瞬間的事情,人眼是無法察覺到任何變化的,但是頁面上的元素確實是發生了變化,然後此時測量卡片的位置資訊,即 Last所需要的資料

Invert

獲取了動畫起始階段受影響的卡片的位置資訊後,就可以通過 transform屬性對元素的位置進行反轉了

// Last + Invert
const stepIndex = updateStatus === 0 ? 1 : 0
activeList.forEach((itemEle, index) => {
  rectInfo = itemEle.getBoundingClientRect()
  transArr[index + stepIndex][0] = transArr[index + stepIndex][0] - rectInfo.left
  transArr[index + stepIndex][1] = transArr[index + stepIndex][1] - rectInfo.top
}
複製程式碼

Play

準備階段就緒,就可以進行最後一步 Play起來了,這一步的關鍵就是給元素加上 transition屬性,並移除 transform給元素帶來的位置變化:

// Play
// 重置
transArr = getArrByLen(this.state.listData.length)
setTimeout(() => {
  this.setState({
    animateStatus: 3
  })
}, 0)
複製程式碼

因為瀏覽器會對頁面的 DOM變化進行合併優化,所以為了能在視覺上呈現出想要的動畫效果,這裡必須要打斷這種優化,setTimeout是一個很常用的方式

到此為止,就完成了文章開頭的那個動畫效果,我做了個 Live Demo,有興趣的可以親自試下,另外程式碼也可以上傳到 Github

實現圖片放大/恢復動畫

微信app裡聊天介面點選預覽圖片時,圖片從對話方塊到全屏預覽的這個過程,用了一個過渡的動畫,呈現出圖片從小圖到大圖和從大圖恢復到小圖的全過程,縮放過程類似於下面這種:

讓動畫變得更簡單之FLIP技術

這種也屬於連續動畫,當然也可以通過 FLIP來輕鬆實現

First

這裡涉及到圖片的位置和尺寸的變化,圖片從 First的小圖原位置和小圖尺寸,變成了 Last狀態下的大圖位置和大圖尺寸,所以需要同時獲取這兩個資料,其實都是可以通過一次呼叫 getBoundingClientRect完成

Last

獲取圖片已經處於預覽狀態下的尺寸和位置資訊,同樣使用 getBoundingClientRect完成

另外,為了更好地利用 transform動畫,我這裡將圖片兩個狀態下的尺寸變化轉變為 scale值的變化,FirstLast狀態下寬度或者高度的比例就是這個 scale的應當取值(在沒有改變圖片寬高比例的前提下)

scaleValue = rectInfo.width / lastRectInfo.width
複製程式碼

Invert

使用 transform進行位置和尺寸(即改變 scale值)的反轉

這裡有一點需要注意的是,由於 transform動畫預設的 transform-origin為元素的中心,即50% 50%,但是計算出來的 lefttop卻是相對於沒有縮放的圖片而言的,所以當 scale取值不唯一時,圖片動畫的 First狀態就會發生偏差,需要將 transform設為 0 0以消除這種偏差

讓動畫變得更簡單之FLIP技術

Play

為圖片新增 transition屬性,並移除相關 transform屬性,即可啟動動畫

可以看到,套用了 FLIP之後,原本看起來比較棘手的一個動畫,被輕易模式化實現了

至於放大後的圖片恢復到小圖這一個階段,可以看成是另外一個 FLIP動畫,繼續套用即可,只不過這個動畫就是上一個放大動畫的逆向,所需的尺寸和位置資訊都已經拿到了,可以省去呼叫 getBoundingClientRect的過程

同樣做了個 Live Demo,有興趣的可以親自試下,另外程式碼也可以上傳到 Github

為什麼要用FLIP

有些人可能比較疑惑,如果想要實現動畫的話,直接 transform不就好了,為什麼要多此一舉搞個 FLIP的概念出來?

我一開始也有這個疑惑,但是當我實際實現一個動畫的時候,比如開頭的那個卡片動畫,這個疑問就立即得到了解答。

對於一些動畫,你明確的知道它的初始態(First)和結束態(Last),比如你就想讓一個元素從 left:10px;移動到 left:100px;,那麼你直接 transform就好了,根本沒必要 FLIP,用了反而多此一舉;

但除此之外,還有一部分你無法明確的初始態(First)或結束態(Last)的動畫,比如開頭那個卡片動畫,除非你限定死了每個卡片的尺寸以及整體頁面的尺寸,否則你無法明確當你任意插入或者刪除了某個卡片之後,其他卡片應當在什麼位置。

比如,在你的瀏覽器下,每個卡片寬高都是 100,瀏覽器頁面寬度為 1380,所以每一列可以排布 13個卡片,但這只是在你的瀏覽器上,使用者的瀏覽器頁面寬度可能是 1280,也可能是1980,每一列排布的卡片數量可能是12也可能是19,不一而足,甚至你還可以任意 resize頁面的尺寸,那麼這個時候,你怎麼確定每個時刻所有卡片 last狀態的資訊?

你可能會說,我當然不知道,但是我可以使用瀏覽器 API進行測量啊。

不好意思,這正是 FLIP要做的事情之一,你還是在無意識地情況下用到了這個東西,只不過相對於被前人總結並優化後的 FLIP來說,你的整體用法可能更零散更不規範一些。

就像標題說的那樣,讓動畫變得更簡單,你可以不用,但是如果你知道怎麼用了,那麼動畫對於你來說就是一個公式一把梭,更 easy

小結

很多前端同學似乎不太在意動畫,認為這只是一個輔助能力,業務邏輯才是最重要的,其他的全都靠後站,就算是有時間也要看心情再決定搞不搞

我的看法是,業務邏輯當然是要放在首位的,但是同樣也不要小看了其餘的細枝末節,例如動畫,一個體驗良好的動效完全可以吸引使用者的更多停留,以一種通用的方式從側面提升業務的轉化效果,某些特定場景下,其所能起到的作用甚至可以與業務的目標並駕齊驅

相關文章