這是一篇很好的互動(dòng)式文章,F(xiàn)ramer Motion 布局動(dòng)畫(huà)
重現(xiàn)framer的神奇布局動(dòng)畫(huà)的指南。
到目前為止,我最喜歡 Framer Motion 的部分是它神奇的布局動(dòng)畫(huà)--將 layout prop 拍在任何運(yùn)動(dòng)組件上,看著該組件從頁(yè)面的一個(gè)部分無(wú)縫過(guò)渡到下一個(gè)部分。
在這篇文章中,我們主要介紹:
- 布局變化:它們是什么,何時(shí)發(fā)生。
- 基于CSS的方法以及為什么它們并不總是有效。
- FLIP:是Framer Motion使用的技術(shù)。
布局變化
當(dāng)頁(yè)面上的一個(gè)元素影響其他元素改變位置時(shí),就會(huì)發(fā)生布局變化。例如,改變一個(gè)元素的寬度或高度就是一種布局變化,因?yàn)槿魏蜗噜彽脑囟急仨氁苿?dòng),以便為該元素的新尺寸騰出空間。
同樣,改變?cè)氐膉ustify-content屬性也是一種布局變化,因?yàn)樗鼘?dǎo)致該元素的子元素改變位置。
不過(guò),像scale屬性的變化并不是布局的改變,因?yàn)樗淖兓挥绊戫?yè)面上的其他元素。
用CSS做動(dòng)畫(huà)
那么,我們?nèi)绾螌⒉季肿兓龀蓜?dòng)畫(huà)呢?一種方法是直接使用 CSS過(guò)渡使屬性產(chǎn)生動(dòng)畫(huà):
現(xiàn)在,當(dāng) square
style.css
看上去,CSS 也可以做動(dòng)畫(huà),但它有兩個(gè)主要的缺點(diǎn):
- 不能把所有東西都做成動(dòng)畫(huà)。例如,不能對(duì)justify-content?的變化制作動(dòng)畫(huà),因?yàn)閖ustify-content不是一個(gè)可動(dòng)畫(huà)的屬性。
- 性能問(wèn)題。涉及布局變化的CSS動(dòng)畫(huà)通常比基于 transform 的動(dòng)畫(huà)更昂貴,所以你可能會(huì)發(fā)現(xiàn)你的動(dòng)畫(huà)在低端設(shè)備上不那么流暢。
我們先來(lái)看看性能問(wèn)題。
性能
不要預(yù)先優(yōu)化 如果在低端設(shè)備上沒(méi)有注意到任何性能問(wèn)題,而且CSS transition 對(duì)你有效,那么就不要擔(dān)心!只有在需要時(shí)才進(jìn)行優(yōu)化。
涉及布局變化的CSS動(dòng)畫(huà)通常比其他CSS動(dòng)畫(huà)更昂貴,因?yàn)樗绊懙街車钠渌?。這是因?yàn)闉g覽器必須在動(dòng)畫(huà)的每一幀中重新計(jì)算頁(yè)面的布局--對(duì)于一個(gè)60FPS的動(dòng)畫(huà)來(lái)說(shuō),這意味著每秒鐘要計(jì)算60次!
回顧上面動(dòng)畫(huà)。注意到灰色的盒子看起來(lái)也在做動(dòng)畫(huà),盡管我們只過(guò)渡了藍(lán)色的盒子:
發(fā)生這種情況的原因是,每次藍(lán)框的尺寸發(fā)生變化時(shí),瀏覽器都會(huì)重新計(jì)算灰框的位置。
另一方面,瀏覽器可以更快地對(duì) transform 等CSS屬性進(jìn)行動(dòng)畫(huà)處理,因?yàn)樗鼈儾挥绊懖季帧?/p>
注意,隨著藍(lán)色方框的增長(zhǎng),灰色方框保持原狀!
所以,如果 transform? 的動(dòng)畫(huà)成本更低,我們是否可以用 transform
是的,可以!
FLIP
FLIP 是 First, Last, Inverse, Play? 的縮寫(xiě),它是一種技術(shù),可以讓我們使用 "快速" 的 CSS 屬性(如transform?)對(duì) "slow" 的布局變化制作動(dòng)畫(huà)。FLIP甚至可以對(duì) "不可動(dòng)畫(huà)" 的屬性(如justify-content)進(jìn)行動(dòng)畫(huà)處理。Framer Motion使用FLIP來(lái)實(shí)現(xiàn)其布局動(dòng)畫(huà)。
顧名思義,F(xiàn)LIP是一種四步技術(shù),它通過(guò)顛倒瀏覽器所做的任何布局變化來(lái)工作。我們通過(guò)動(dòng)畫(huà)演示justify-content從flex-start到flex-end的變化來(lái)弄清楚它是如何工作的。
First
在 First 中,在任何布局變化發(fā)生之前,測(cè)量我們要做動(dòng)畫(huà)的元素的位置:
獲取元素位置的一種方法是使用HTML元素的.getBoundingClientRect()方法:
Last
在 Last 這一步中,我們測(cè)量布局變化后元素的位置:
為了在代碼中實(shí)現(xiàn)這一點(diǎn),我們首先假設(shè)布局的改變意味著組件剛剛重新渲染了。所以我們先從useEffect鉤子中刪除依賴數(shù)組,使鉤子每次渲染都能運(yùn)行。
試著觸發(fā)幾次布局變化,檢查控制臺(tái),看看顯示的x和y值是什么。
App.js
Motion.js
Inverse
在 inverse 階段,我們修改正方形的位置,使其看起來(lái)像是根本沒(méi)有移動(dòng)過(guò)。要做到這一點(diǎn),我們要比較我們所做的兩個(gè)測(cè)量,并計(jì)算出一個(gè) transform ,然后應(yīng)用到正方形上。
使用 React 實(shí)現(xiàn)的代碼:
App.js
Motion.js
Play
到目前為止,我們有一個(gè)正方形,它被施加了一個(gè) transform,在按下切換鍵后沒(méi)有移動(dòng)。
在FLIP的最后一步,即 Play 步驟中,我們將這個(gè) transform 動(dòng)畫(huà)化為零,讓正方形動(dòng)畫(huà)化到它的最終位置。
有多種方法可以實(shí)現(xiàn)這個(gè)動(dòng)畫(huà);我個(gè)人選擇使用Popmotion的animate函數(shù)。
把所有東西放在一起
把所有步驟做起來(lái),我們得到:
動(dòng)畫(huà)的大小
到目前為止,我們只用FLIP來(lái)制作位置變化的動(dòng)畫(huà)。但對(duì)于大小來(lái)說(shuō),我們可以用同樣的方法嗎我們?cè)囍鴱?fù)制下面的動(dòng)畫(huà),在這個(gè)動(dòng)畫(huà)中,正方形被拉伸到充滿整個(gè)容器。
測(cè)量尺寸變化
我們首先要測(cè)量布局改變前后的正方形的大小。碰巧是提,我們用來(lái)測(cè)量正方形的.getBoundingClientRect()?方法也剛好返回元素的 width? 和 height:
反轉(zhuǎn)尺寸變化
為了反轉(zhuǎn)尺寸變化,我們將用最終尺寸除以初始尺寸:
得到一個(gè)比例后,我們可以將其傳遞給 scale 屬性:
我們不會(huì)像position?那樣將比例動(dòng)畫(huà)到0?,而是將比例動(dòng)畫(huà)到1(如果我們將比例動(dòng)畫(huà)到0,元素將完全消失):
使用 position 固定大小
到目前為止,我們已經(jīng)能夠使用FLIP為位置和大小的變化制作動(dòng)畫(huà)。當(dāng)我們?cè)噲D將大小和位置都做成動(dòng)畫(huà)時(shí)會(huì)發(fā)生什么?
嗯,這看起來(lái)有點(diǎn)不對(duì)勁。這里發(fā)生了什么?如果我們?cè)?nbsp;play? 步驟之前暫停動(dòng)畫(huà),我們可以看到在 inverse
修復(fù)轉(zhuǎn)換的起點(diǎn)
我們?cè)囍闱宄@個(gè)問(wèn)題。
當(dāng)我們把位置和大小的變化結(jié)合起來(lái)時(shí),我們?cè)谀嫦虿襟E中進(jìn)行了兩個(gè)獨(dú)立的變換--平移和縮放。如果我們單獨(dú)看一下這些變換,我們就可以知道這個(gè)正方形是如何結(jié)束的:
我們的算法首先將最終位置的左上角與原始位置的左上角對(duì)齊,然后將其縮小到初始尺寸。
縮放變換似乎是這里的罪魁禍?zhǔn)?-它從正方形的中心開(kāi)始縮放,導(dǎo)致正方形最終出現(xiàn)在錯(cuò)誤的位置?,F(xiàn)在,如果我們把變換的原點(diǎn)改為左上角,使其與平移相一致......
對(duì)了!這就對(duì)了
如果 Transform Origin 發(fā)生變化怎么辦?
當(dāng)然,這個(gè)解決方案的最大問(wèn)題是,我們已經(jīng)硬編碼了 transform origin 的值。如果用戶想要一個(gè)不同的變換原點(diǎn)呢?在這種情況下,布局動(dòng)畫(huà)應(yīng)該仍然有效。
訣竅在于確保 inverse 步驟比較了兩個(gè)方塊的變換原點(diǎn)之間的距離。換句話說(shuō),這個(gè)錯(cuò)誤的發(fā)生是因?yàn)闇y(cè)量的距離和變換原點(diǎn)之間的差異:getBoundingClientRect()返回元素的左上角,而變換原點(diǎn)默認(rèn)是在元素的中心。
只有當(dāng)兩個(gè)正方形的大小相同時(shí),左上角的點(diǎn)之間的距離和中心之間的距離才是相等的。
為了簡(jiǎn)單起見(jiàn),我在這里只比較水平距離--如果我們考慮到垂直距離,同樣的概念也適用。
當(dāng)最終的正方形較大時(shí),中心之間的距離大于左上角各點(diǎn)之間的距離。同樣,當(dāng)最終的正方形較小時(shí),中心之間的距離小于左上角各點(diǎn)之間的距離。
有了這個(gè)見(jiàn)解,我們也可以通過(guò)使用中心之間的距離而不是左上角的點(diǎn)來(lái)解決這個(gè)問(wèn)題。
糾正子元素的變形
到目前為止,我們已經(jīng)能夠制作一個(gè)布局動(dòng)畫(huà),可以無(wú)縫過(guò)渡到大小和位置的變化?,F(xiàn)在讓我們?cè)黾右粋€(gè)測(cè)試--如果我們的元素有子元素會(huì)怎樣?
如上圖可以看到文字大小被改了。我們?cè)鯓硬拍芙鉀Q這個(gè)問(wèn)題呢?
導(dǎo)致該問(wèn)題的原因還 是inverse 比例變換。當(dāng)我們反轉(zhuǎn)到一個(gè)較小的正方形時(shí),文本最終會(huì)變小,因?yàn)檎叫伪话幢壤s小。同樣地,當(dāng)我們反轉(zhuǎn)到一個(gè)較大的正方形時(shí),文本最終會(huì)變大,因?yàn)檎叫伪话幢壤糯罅恕?/p>
反比例公式
一種方法是在子元素上應(yīng)用另一種變換,"抵消"父元素的變換。子元素的變換公式:
例如:父元素變大兩倍,那么子方需要將其尺寸減半,才能保持相同的尺寸。試著移動(dòng)下面的滑塊,注意文字是如何保持相同大小的,而不管廣場(chǎng)的大小如何。
現(xiàn)在,如何將其與我們的布局動(dòng)畫(huà)相結(jié)合呢?
嘗試
我嘗試的第一件事是,在父元素要做動(dòng)畫(huà)之前,先計(jì)算一次反比例,然后在子元素上單獨(dú)運(yùn)行一個(gè)動(dòng)畫(huà)。
例如,如果父元素動(dòng)畫(huà)從scaleX: 2到scaleX: 1?,那么子代將從scaleX: 1 / 2到scaleX:1,只要比例校正的時(shí)間與父元素動(dòng)畫(huà)相同,這種方法應(yīng)該是可行的。
但是,運(yùn)行起來(lái)效果卻是錯(cuò)誤的:
在整個(gè)動(dòng)畫(huà)過(guò)程中,文字明顯地在改變。
正確的縮放時(shí)間
這里的問(wèn)題就在于這個(gè)假設(shè):
只要比例校正的時(shí)間與父動(dòng)畫(huà)相同,這種方法應(yīng)該是有效的。
正常情況下,"正確" 反轉(zhuǎn)比例不會(huì)以與父動(dòng)畫(huà)相同的方式變化,它有點(diǎn)像做自己的事情。
在上面的例子中,藍(lán)線表示父方的比例,而黃線表示子方的比例。請(qǐng)注意,藍(lán)線是一條直線,而黃線則有點(diǎn)像曲線。這告訴我們,反比例的時(shí)間與父比例的時(shí)間是不一樣的!
為了解決這個(gè)問(wèn)題,我們可以這么做:
- 提前計(jì)算出正確的時(shí)間
- 每當(dāng)父元素比例發(fā)生變化時(shí),計(jì)算反比例。
(2)恰好比(1)簡(jiǎn)單得多,而且還允許我們?cè)诟冈厣咸幚砀鞣N不同的時(shí)序。這也是 Framer Motion使用的方法。
App.js
Motion.js
其實(shí)不是這樣的?
在這種情況下,使比例校正工作的方式是通過(guò)將子元素包裹在<div>?中,并將比例校正應(yīng)用于<div>中,這會(huì)有一些問(wèn)題:
一個(gè)運(yùn)動(dòng)組件在DOM中有兩個(gè)元素,從用戶體驗(yàn)的角度來(lái)看,這可能是個(gè)問(wèn)題
所有子組件都進(jìn)行了比例校正,不可能一個(gè)子組件被校正而另一個(gè)子組件不被校正
如果子組件也在做動(dòng)畫(huà),可能會(huì)有問(wèn)題--我沒(méi)有測(cè)試過(guò),但我認(rèn)為比例校正會(huì)導(dǎo)致問(wèn)題,因?yàn)槲覀兣で俗咏M件的坐標(biāo)空間
Framer Motion 的做法有點(diǎn)不同,我們必須讓子組件成為布局組件來(lái)選擇加入比例校正。
這個(gè)API意味著子組件需要能夠 "鉤住 "父組件的動(dòng)畫(huà),這讓實(shí)現(xiàn)變得更加復(fù)雜。
我選擇不以這種方式實(shí)現(xiàn),因?yàn)槲也幌朊撾x核心的比例校正概念。如果你有興趣,可以看看 Framer Motion源代碼,他們使用一種叫做 "投影節(jié)點(diǎn)( "projection nodes")"的東西來(lái)維護(hù)自己的類似DOM的運(yùn)動(dòng)組件樹(shù)。
今天的內(nèi)容就到這里,感謝大家的閱讀。
來(lái)源:https://www.nan.fyi/magic-motion