animation-fill-mode的一些思考,animation-fill-mode
animation-fill-mode是css3動畫的一個屬性,它能夠控制元素在動畫執行前與動畫完成後的樣式。一個帶有延遲,並且按正常方向執行的動畫(正常方向是指從0%運行到100%),執行一次的過程可以描述如下:
#demo1 { .target.animate { animation-name: move_1; animation-duration: 2s; animation-delay: 1s; &.target_1 { animation-fill-mode: backwards; } &.target_2 { animation-fill-mode: forwards; } &.target_3 { animation-fill-mode: both; } }}@keyframes move_1 { 0% { transform: translate(-50px, 0); } 50% { transform: translate(0, 0); } 100% { transform: translate(50px, 0); }}
在以上demo中,定義了一個move_1的動畫,它包含三個主要畫面格,第一幀讓元素往左位移50px,最後一幀讓元素往右位移50px,這個位移都是相對元素的初始狀態而言的(就是沒有添加動畫的狀態)。demo中從上到下有三個元素,分別應用了animation-fill-mode屬性的三個值:backwards,forwards,both。結合前面對這三個屬性值的說明,相信不難理解demo中三個元素的動畫效果:
backwards和both使得第一個元素和第三個元素,在動畫添加後都立即變為動畫第一幀的狀態。而第二個元素沒有應用第一幀的狀態。
forwards和both使得第二個元素和第三個元素,在動畫結束後仍然保持動畫最後一幀的狀態。而第1個元素沒有。
以上內容可能會讓人覺得animation-fill-mode是一個比較簡單的屬性,因為它的作用非常的簡單明了。儘管如此,我在最近做一些動畫效果的時候卻發現,這個屬性在真正使用的時候要想完全融會貫通地去使用,還真不是那麼容易,尤其是當我們需要同時應用多個動畫,定義連續的複雜動畫時,就可能會在寫動畫的過程中,碰到一些自己按理論不能理解清楚的點,雖然最後吧,我們總是能想辦法搞定我們遇到的問題,那是因為這個屬性畢竟只有那麼幾個值,多加調試當然能解決問題,但是做完了,心裏面還是想解決那個為什麼我之前那麼寫就不行的問題。所以本文從一些非常規的角度來研究animation-fill-mode在實際使用過程中可能會存在理解偏差的問題,我不敢保證在本文中,我提出的一些理解方式一定是正確的,只是以我現在的經驗,只能得出這麼些結論。希望本文可以拋磚引玉,發現一些更可靠更完美的思想。
首先,我想對animation-fill-mode的理論知識再做一次補充說明:
1)在animation-fill-mode的基礎知識中,有這麼幾個關鍵詞:動畫等待時間(也叫動畫延遲時間),動畫結束後,第一幀,最後一幀。這些關鍵詞的更深的含義是:
a. backwards一定是在動畫延遲時間內才會生效;
b. forwards一定在動畫完成之後才會生效,對於一個迴圈的動畫來說,它沒有動畫完成後的狀態,所以forwards不會起作用;
c. 第一幀和最後一幀不是絕對的,就是說第一幀不一定永遠跟0%這幀對應,最後一幀不一定永遠跟100%這幀對應。具體到底0%是第一幀還是100%是第一幀,跟另外兩個動畫屬性有關係:animation-direction和 animation-iteration-count。舉個例子:當animation-direction是alternate,animation-iteration-count是2的時候,第一幀和最後一幀就都是0%。至於為啥是這樣,自己簡單畫個圖就好理解了:
#demo2 { .target.animate { animation-name: move_2; animation-duration: 2s; animation-delay: 1s; &.target_1 { animation-fill-mode: backwards; } &.target_2 { animation-fill-mode: forwards; } &.target_3 { animation-fill-mode: both; } }}@keyframes move_2 { 50% { transform: scale(1.2, 1.2); }}
在這個demo中,你會看到三個元素應用了不同的animation-fill-mode,最終的效果卻完全相同。這是因為動畫裡面沒有定義0%,和100%,導致animation-fill-mode找不到它所需要的第一幀和最後一幀。儘管上面的demo動畫中,50%在動畫定義裡面,它是唯一的一幀,按照我們對唯一性的認識,那麼50%這幀既可以看作是一幀,也可以看作是最後一幀,但是animation-fill-mode只認動畫定義裡的0%和100%,而不是動畫定義裡的第一幀和最後一幀。
那麼是不是意味著沒有0%和100%的話,animation-fill-mode就一定沒有作用呢?
其實不是。當動畫定義裡面沒有0%和100%的時候,並不是意味著動畫就沒有起始幀跟結束幀了,任何一個動畫一定具有起始幀和結束幀,預設情況下起始幀跟結束幀所對應的樣式就是元素未添加的動畫前的樣式,我們可以通過0%或100%,來覆蓋預設的起始幀和結束幀的定義。也就是說,當沒有0%或100%的時候,animation-fill-mode還是起作用的,只不過它是用元素的初始狀態來起作用,所以你看不出來而已。
上一段的結論,我並沒有在w3c上看到有介紹,而是我根據自己的一些思考跟觀察猜測出來的。接下來我會用chrome的動畫調試工具來輔助說明我的判斷,在後面介紹animation-fill-mode在多個動畫中的實踐時,我還會結合一個例子並用這些理論來解釋。
新版的chrome提供了動畫調試的功能,開啟檔案如下:
#demo3 { .target.animate { animation-name: move_3; animation-duration: 2s; animation-delay: 1s; &.target_1 { animation-fill-mode: none; } &.target_2 { animation-fill-mode: backwards; } &.target_3 { animation-fill-mode: forwards; } &.target_4 { animation-fill-mode: both; } }}@keyframes move_3 { 50% { transform: scale(1.2, 1.2); }}
不過這一塊的目的不是為了說明這個demo的動畫效果,而是為了介紹如何從動畫控制台去看動畫作用過程的範圍:
#demo4 { .target { transform: scale(1.2, 1.2); } .target.animate { animation-duration: 2s; animation-delay: 1s; &.target_1 { animation-fill-mode: none; animation-name: move_4_01; } &.target_2 { animation-fill-mode: backwards; animation-name: move_4_01; } &.target_3 { animation-fill-mode: forwards; animation-name: move_4_01; } &.target_1_02 { animation-fill-mode: none; animation-name: move_4_02; } &.target_2_02 { animation-fill-mode: backwards; animation-name: move_4_02; } &.target_3_02 { animation-fill-mode: forwards; animation-name: move_4_02; } }}@keyframes move_4_01 { 0% { transform: translate(-50px, 0); } 50% { transform: translate(0, 0); } 100% { transform: translate(50px, 0); }}@keyframes move_4_02 { 0% { transform: translate(-50px, 0) scale(1.2, 1.2); } 50% { transform: translate(0, 0) scale(1.2, 1.2); } 100% { transform: translate(50px, 0) scale(1.2, 1.2); }}
在上面的中,我用箭頭把元素跟動畫控制台中的動畫元素時間軸做了一個對應關係,方便理解。
在這個demo裡面,元素預設都有一個transform: scale(1.2,1.2)的設定,然後定義兩個動畫,move_4_01這個動畫的樣式定義裡面同樣包含了一個transform屬性,而且沒有保留元素預設的transform設定。move_4_02這個動畫的樣式定義裡面也包含了一個transform的設定,跟move_4_01不同的是,這個動畫還保留了元素預設的scale(1.2,1.2)的設定。
demo中用到了六個元素,它們按照animation-fill-mode分成了三組,以便對應不同的動畫作用過程;每組裡面的兩個元素,分別應用move_4_01和move_4_02兩個動畫。
通過在時間軸上的動畫等待階段,動畫執行階段和動畫結束階段,任取三個時間點,觀察元素的表現,可以協助分析在動畫作用過程中,當動畫屬性與初始屬性衝突時存在的規律:
動畫等待階段:
#demo5 { .target.animate { animation-duration: 1s, 1s; animation-delay: 1s, 2s; animation-fill-mode: both; &.target_1 { animation-name: move_5, bg_change_5; } &.target_2 { animation-name: bg_change_5, move_5; } }}@keyframes move_5 { 0% { transform: translate(-50px, 0); } 50% { transform: translate(0, 0); } 100% { transform: translate(50px, 0); }}@keyframes bg_change_5 { 0% { background-color: orange; transform: scale(0.8); } 100% { background-color: red; transform: scale(1.2); }}
在這個demo裡,定義了2個動畫,move_5變元素在x軸的位移,bg_change_5同時改變元素的縮放和背景色。兩個元素都同時應用了這兩個動畫,除了應用時的順序不同,其它的參數如延遲,期間,animation-fill-mode都完全相同。結合動畫控制台顯示的時間軸,我們來看看這兩個元素在動畫作用過程中都有什麼規律。
以動畫等待階段為例:
#demo6 { .target:not(.target_1) { visibility: hidden; opacity: 0; } .target.animate { &.target_2 { animation-name: move_6_01, move_6_02; animation-duration: 1s, 1s; animation-delay: 0s, 2s; animation-fill-mode: both; } }}@keyframes move_6_01 { 0% { visibility: hidden; opacity: 0; transform: translate(-50px, 0); } 100% { visibility: visible; opacity: 1; transform: translate(0, 0); }}@keyframes move_6_02 { 100% { visibility: hidden; opacity: 0; transform: translate(50px, 0); }}
正如你所看到的,我定義了兩個動畫move_6_01,move_6_02,這兩個動畫按照01 02 的順序應用到.target_2這個元素上,兩個動畫期間都是1s,第一個動畫延遲時間為0,第二個動畫延遲時間為2s,以便達到淡入淡出的時間都為1s,然後中間停留的時間也是1s的效果;兩個動畫同時應用了animation-fill-mode:both。然後還給.target_2這個元素設定了初始的屬性:visibility: hidden,opactity: 0。.target_1隻是一個對比的元素,沒有添加動畫效果。
當我運行這個代碼的時候最後卻發現,一點動畫效果都沒有,動畫控制台已經監聽到動畫了,但是元素看不見動畫效果:
#demo7 { .target:not(.target_1) { visibility: hidden; opacity: 0; } .target.animate { &.target_2 { animation-name: move_6_01, move_6_02; animation-duration: 1s, 1s; animation-delay: 0s, 2s; animation-fill-mode: both,forwards; } }}
在這一步,淡入跟停留的效果出來了,可是淡出的效果沒有出來。
雖然最後,我在不明白這其中的原理的前提下,將動畫調試出來了,但是對於這個問題還是覺得很有必要去深入研究一下,這也是我寫本文的初衷。有了之前的那些結論,接下來就看看如何分析前面兩步的現象產生的原因。
先來看看demo6(就是本部分的第一個)的動畫時間軸:
#demo8 { .target:not(.target_1) { visibility: hidden; opacity: 0; } .target.animate { &.target_2 { animation-name: move_8_01, move_8_02; animation-duration: 1s, 1s; animation-delay: 0s, 2s; animation-fill-mode: both,forwards; } }}@keyframes move_8_01 { 0% { visibility: hidden; opacity: 0; transform: translate(-50px, 0); } 100% { visibility: visible; opacity: 1; transform: translate(0, 0); }}@keyframes move_8_02 { 0% { visibility: visible; opacity: 1; transform: translate(0, 0); } 100% { visibility: hidden; opacity: 0; transform: translate(50px, 0); }}
通過這個例子,希望能讓大家明白本文總結的一些理論的實踐方法。
總結
其它的就不多說了,把本文提出的一些結論稍微提煉一下,希望這些東西對大家今後做複雜的動畫效果有所協助:
1. 動畫是獨立,各自都有各自的時間軸,互不影響。
2. animation-fill-mode只認動畫定義裡的0%和100%,而不是動畫定義裡的第一幀和最後一幀。
3. 當動畫定義裡面沒有0%和100%的時候,並不是意味著動畫就沒有起始幀跟結束幀了,任何一個動畫一定具有起始幀和結束幀,預設情況下起始幀跟結束幀所對應的樣式就是元素未添加的動畫前的樣式,我們可以通過0%或100%,來覆蓋預設的起始幀和結束幀的定義。
4. 動畫控制台中,時間軸實心的部分就是動畫的作用過程。在動畫屬性與初始屬性衝突的時候,只要一個元素處於動畫作用過程中,動畫裡定義的屬性優先順序始終高於元素的初始屬性。
5. 多動畫效果中,要判斷任意時刻,哪個動畫的優先順序高,只要在動畫控制台中,找到該時刻對應的線,該線與所有動畫的實心部分(動畫作用過程)的交集,按照從上往下的順序,越往下的點的優先順序越高。
謝謝閱讀。