某次被問到如何實現如下動畫效果:javascript
若干個元素卡片從上而下排列,當增長或刪除某個卡片的時候,其他的卡片會以一種 transition
動畫的形式移動到適當的位置上,而不是生硬地閃現html
當時我剛好看過 Vue
中的內置組件 transition
的實現,意識到徹底能夠用 transition
組件的部分原理來完成這個效果,可是因爲沒有深刻地探究過爲何是這樣,只停留在表面,知其然而不知其因此然,因此儘管我知道如何實現這個效果,但很難解釋爲何是這樣,語言組織地比較困難前端
後來我無心間看到一篇文章 FLIP技術給Web佈局帶來的變化,立馬恍然大悟,原來這個東西叫 FLIPjava
FLIP是 First
、Last
、Invert
和 Play
四個單詞首字母的縮寫git
First
,指的是在任何事情發生以前(過渡以前),記錄當前元素的位置和尺寸,即動畫開始以前那一刻元素的位置和尺寸信息,可使用 getBoundingClientRect()
這個 API
來處理(大部分狀況下其實 offsetLeft
和 offsetTop
也是能夠的)github
Last
:執行一段代碼,讓元素髮生相應的變化,並記錄元素在動畫最後狀態的位置和尺寸,即動畫結束以後那一刻元素的位置和尺寸信息瀏覽器
Invert
:計算元素第一個位置(First
)和最後一個位置(Last
)之間的位置變化(若是須要,還能夠計算兩個狀態之間的尺寸大小的變化),而後使用這些數字作必定的計算,讓元素進行移動(經過 transform
來改變元素的位置和尺寸),從而建立它位於第一個位置(初始位置)的一個錯覺微信
即,一上來直接讓元素處於動畫的結束狀態,而後使用 transform
屬性將元素反轉回動畫的開始狀態(這個狀態的信息在 First
步驟就拿到了)app
Play
:將元素反轉(僞裝在first
位置),咱們能夠把 transform
設置爲 none
,由於失去了 transform
的約束,因此元素確定會往本該在的位置(即動畫結束時的那個狀態)進行移動,也就是last
的位置,若是給元素加上 transition
的屬性,那麼這個過程天然也就是以一種動畫的形式發生了佈局
按照個人理解,就是對動畫元素起止狀態的一個量化,量化成一個公式,絕大部分的連續動畫均可以經過套用這個公式來完成,提高動畫的開發效率,更加詳細的請自行參見 FLIP技術給Web佈局帶來的變化
瞭解了 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
來輕鬆實現
First
這裏涉及到圖片的位置和尺寸的變化,圖片從 First
的小圖原位置和小圖尺寸,變成了 Last
狀態下的大圖位置和大圖尺寸,因此須要同時獲取這兩個數據,其實都是能夠經過一次調用 getBoundingClientRect
完成
Last
獲取圖片已經處於預覽狀態下的尺寸和位置信息,一樣使用 getBoundingClientRect
完成
另外,爲了更好地利用 transform
動畫,我這裏將圖片兩個狀態下的尺寸變化轉變爲 scale
值的變化,First
與 Last
狀態下寬度或者高度的比例就是這個 scale
的應當取值(在沒有改變圖片寬高比例的前提下)
scaleValue = rectInfo.width / lastRectInfo.width
複製代碼
Invert
使用 transform
進行位置和尺寸(即改變 scale
值)的反轉
這裏有一點須要注意的是,因爲 transform
動畫默認的 transform-origin
爲元素的中心,即50% 50%
,可是計算出來的 left
和 top
倒是相對於沒有縮放的圖片而言的,因此當 scale
取值不惟一時,圖片動畫的 First
狀態就會發生誤差,須要將 transform
設爲 0 0
以消除這種誤差
Play
爲圖片添加 transition
屬性,並移除相關 transform
屬性,便可啓動動畫
能夠看到,套用了 FLIP
以後,本來看起來比較棘手的一個動畫,被輕易模式化實現了
至於放大後的圖片恢復到小圖這一個階段,能夠當作是另一個 FLIP
動畫,繼續套用便可,只不過這個動畫就是上一個放大動畫的逆向,所需的尺寸和位置信息都已經拿到了,能夠省去調用 getBoundingClientRect
的過程
一樣作了個 Live Demo,有興趣的能夠親自試下,另外代碼也能夠上傳到 Github
有些人可能比較疑惑,若是想要實現動畫的話,直接 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
。
不少前端同窗彷佛不太在乎動畫,認爲這只是一個輔助能力,業務邏輯纔是最重要的,其餘的全都靠後站,就算是有時間也要看心情再決定搞不搞
個人見解是,業務邏輯固然是要放在首位的,可是一樣也不要小看了其他的細枝末節,例如動畫,一個體驗良好的動效徹底能夠吸引用戶的更多停留,以一種通用的方式從側面提高業務的轉化效果,某些特定場景下,其所能起到的做用甚至能夠與業務的目標並駕齊驅