Android Jetpack - 佈局動畫與佈局過渡
本篇主題依然是動畫,主角是Android系統的 佈局動畫 (Layout Animation)和 佈局過渡 (Layout Transition)。
官方文件中,對於這兩個概念其實有所混淆。按照官方籠統的說法,Android的“佈局動畫”是一個預載入的動畫,每當佈局改變的時候,動畫都會執行。但是這個“改變”其實又得分作兩部分來說:一方面,初始化時,從無到有的介面載入,是改變;另一方面,已載入完成的介面的佈局改變,也是改變。雖然官方介紹的時候把這兩個混在一起說,但是它們的實現和使用方法卻並不相同。這也是本篇分作佈局動畫和佈局過渡兩方面來討論的原因。
佈局動畫
佈局動畫是指 ViewGroup首次載入佈局完成時的動畫 ,動畫目標是ViewGroup的子View。一般常見於介面首次載入拉起的時候。
佈局動畫的設定,既可以用XML,也可以程式碼直接控制。
XML設定
XML方式設定佈局動畫是非常簡單的,通過屬性 android:layoutAnimation 來設定。來看看例子。
首先,定義一個線性佈局作為佈局動畫的容器,然後include一個預設的子View,item_sun就是一個ImageView,很簡單,這裡就不寫出來了。
<LinearLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layoutAnimation="@anim/layout_anim_container" android:orientation="horizontal"> <include layout="@layout/item_sun" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
其中 layoutAnimation 屬性指定了佈局動畫的設定。
layou_anim.container.xml
<?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:animation="@anim/anim_translate" android:interpolator="@android:interpolator/accelerate_decelerate" />
佈局動畫的xml定義,以 layoutAnimation 為根標籤。其中屬性animation就是用於設定我們的預定義動畫,這裡是1000ms內右移25畫素。
anim_translate.xml
<?xml version="1.0" encoding="utf-8"?> <translate xmlns:android="http://schemas.android.com/apk/res/android" android:fromXDelta="0" android:toXDelta="25" android:duration="1000"/>
至此,已經全部設定完畢,來看看效果:

【注】這裡介面是已退出狀態,從最近使用重新啟動,也是首次載入。
可以看到,佈局動畫在佈局載入完成後(介面完全顯示)成功執行。那麼,如果最初狀態包含多個子View是什麼效果呢?現在在容器中再增加同樣的include,看看效果:

三個子View同時執行了佈局動畫。
在layoutAnimation中,還有其他屬性可以讓佈局動畫更加生動
- delay :延遲每個子View動畫起點的比例值
- animationOrder :子View動畫的執行順序
delay 是比例值,不是指實際動畫時長。這個比例值的基數,才是動畫時長。例如,如果delay是0.1,動畫時長是800ms,那麼實際的延時就是80ms。設容器中子View的位置為i(按動畫執行順序),那子View的動畫啟動延時就分別是i * 80ms。
animationOrder 用於設定執行順序,包括三個值:normal,reverse,random。很好理解,分別是順序執行(預設)、反序執行和隨機序執行。
現在新增delay屬性,並設定值為1
<?xml version="1.0" encoding="utf-8"?> <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android" android:delay="1" android:animation="@anim/anim_translate" android:interpolator="@android:interpolator/accelerate_decelerate" />

因為動畫時長為1000ms, delay 為1,所以每個子View都在它的前一個執行開始時延遲1000ms後執行自己的動畫。
分別設定 animationOrder 為reverse和random,看看效果:


程式碼控制
通過程式碼來控制佈局動畫也很簡單。
新增一個線性佈局作為動畫目標區域,主要使用 LayoutAnimationController 類來進行控制。將預定義的動畫傳入該控制器,然後通過View的 setLayoutAnimation 方法啟用佈局動畫,最後呼叫控制器的 start 方法啟動佈局動畫。
// Use codes to set layout animation Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_rotation); LayoutAnimationController animationController = new LayoutAnimationController(animation); mContainer2.setLayoutAnimation(animationController); animationController.start();
這裡的預定義動畫是一個沿中軸的360度旋轉。
anim_rotation.xml
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:pivotX="50%" android:pivotY="50%" android:fromDegrees="0" android:toDegrees="360" />
效果如下,最初始的“月亮”按預期旋轉了360度。

image
同樣,控制器還可以設定延時和動畫順序
animationController.setDelay(1); animationController.setOrder(LayoutAnimationController.ORDER_RANDOM);

值得注意的是,隨機效果的每一個子View都是均等的,它們的執行時間完全隨機,可以是執行序列i個位置的任一個 —— 所以,可能出現多個子View一起執行的情況(正如上面效果圖所示)
佈局過渡
初始載入的佈局動畫講完,下面就該佈局動態變化的動畫 —— 佈局過渡登場了。
過渡的分類
引起ViewGroup的佈局變化,主要無外乎三類情況: 增 、 刪 、 改 。增,即新增子View;刪,即刪除子View;而改,即改變子View大小從而引起佈局變化的連鎖反應。在佈局動畫的世界裡,增與刪分別就是 出場 (appearing)和 退場 (disappearing),而改即 改變 (change)。而且,子View的可見性變化,也會引起出場與退場。
由此,就可以衍生出五種過渡型別,如下表所示。
型別 | 說明 |
---|---|
APPEARING | 出場動畫,源於新增或可見 |
DISAPPEARING | 退場動畫,源於刪除或不可見 |
CHANGE_APPEARING | 出場聯變動畫,源於其他出場View |
CHANGE_DISAPPEARING | 退場聯變動畫,源於其他退出View |
CHANGE | 改變聯變動畫,源於非增刪型佈局變化 |
CHANGE_APPEARING和CHANGE_DISAPPEARING都是作用於非增、刪項。也就是說,一個View被新增或刪除,導致佈局變化,引動其他兄弟子View。源是這個View,動畫作用物件卻是兄弟View。
DISAPPEARING和CHANGE_APPEARING是立即執行的動畫,而其他動畫則是時延動畫,時延值是預設的出現、消失動畫時長(300ms)。如果設定了自定義動畫,且動畫時長不等於300ms,那麼延時動畫APPEARING和CHANGE_DISAPPEARING的執行時間點就有可能先於或晚於那兩個立即動畫的結束點的。
為什麼這樣?舉個例子,當有一個新的子View新增時,其他老的子View應該首先動起來(CHANGE_APPEARING),相當於給新View的騰位置(雖然不一定真的騰),然後新的View才能得以進來,並執行出場動畫。同樣道理,退場動畫先執行,因退場而改變的其他View的動畫才能執行。
出場和退場動畫
系統預設
系統預設會提供一個漸顯漸隱的出場和退場動畫,但是得首先啟用佈局過渡功能。在目標容器的佈局中設定即可,很簡單。
android:animateLayoutChanges="true"
嗯,如果使用系統預設的過渡效果,我們的工作就算完成了。不斷增刪子View,來看看效果:

效果還可以,至少,沒有未開啟過渡時的那種突兀感。
【注】字面上講, animateLayoutChanges 屬性意思為“讓佈局改變動起來”,即把過渡動畫作用於佈局改變。需要注意的是,這個屬性和前面的layoutAnimation屬性沒有半毛錢關係。而且, 此屬性開啟是佈局過渡得以執行的先決條件 。
自定義
下面,不用系統預設的漸變效果,自己實現一個簡單的出場動畫:右移一定距離後再歸位。
首先,構造一個 LayoutTransition 物件,並設定給目標容器
mLayoutTransition = new LayoutTransition(); mContainer2.setLayoutTransition(mLayoutTransition);
接著,生成自定義動畫(右移30畫素再歸位),呼叫 LayoutTransition 的 setAnimator 方法設定過渡動畫。
ObjectAnimator animator = ObjectAnimator.ofFloat(null, "translationX", 0, 30, 0); animator.setInterpolator(new AccelerateInterpolator()); mLayoutTransition.setDuration(LayoutTransition.APPEARING, 800); mLayoutTransition.setAnimator(LayoutTransition.APPEARING, animator);
新增子view觀察效果

【注】出場動畫設定的時長為800ms,是呼叫了 LayoutTransition 的 setDuration 方法。在原始動畫效果上呼叫setDuration是無效的。
類似的,增加DISAPPEARING型別,就是退場動畫了。
AnimatorSet animator1 = new AnimatorSet(); animator1.playTogether(ObjectAnimator.ofFloat(null, "scaleX", 1, 0), ObjectAnimator.ofFloat(null, "scaleY", 1, 0)); animator1.setInterpolator(new AccelerateInterpolator()); mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animator1);
退場動畫為收縮子View尺寸至0,看看效果

出場聯變和退場聯變動畫
出場聯變和退場聯變的設定方法類似於出場與退場。
定義一個旋轉一週的CHANGE_APPEARING動畫
ObjectAnimator animator2 = ObjectAnimator.ofFloat(null, "rotation", 0, 360); animator2.setInterpolator(new AccelerateInterpolator()); mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, animator2);
期望是,新增新View的時候,舊的兄弟View就按上面定義的一樣,旋轉360度。不過,這個效果暫時看不了了,因為,上面這段程式碼不起作用!
下面這段才是成功生效的程式碼
AnimatorSet animator2 = new AnimatorSet(); animator2.playTogether(ObjectAnimator.ofFloat(null, "rotation", 0, 360, 0)); layoutTransition.setDuration(LayoutTransition.CHANGE_APPEARING, 800); layoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, animator2);
兩段程式碼區別如下:
- 屬性值引數從"0, 360"變為"0, 360, 0"
- 使用AnimatorSet動畫組代替ObjectAnimator

正如前面所說,新增子View時,先執行CHANGE_APPEARING,然後才執行APPEARING。而且,APPEARING在CHANGE_APPEARING還未執行完的時候就已經開始了(因為預設延時300ms,而CHANGE_APPEARING時長為800ms)。
可是,為什麼第一段程式碼不生效呢?
我們先來看看方法 setAnimator 的說明文件。
Sets the animation used during one of the transition types that may run. Any Animator object can be used, but to be most useful in the context of layout transitions, the animation should either be a ObjectAnimator or a AnimatorSet of animations including PropertyAnimators. Also, these ObjectAnimator objects should be able to get and set values on their target objects automatically. For example, a ObjectAnimator that animates the property "left" is able to set and get the left property from the View objects being animated by the layout transition. The transition works by setting target objects and properties dynamically, according to the pre- and post-layoout values of those objects, so having animations that can handle those properties appropriately will work best for custom animation. The dynamic setting of values is only the case for the CHANGE animations; the APPEARING and DISAPPEARING animations are simply run with the values they have .
It is also worth noting that any and all animations (and their underlying PropertyValuesHolder objects) will have their start and end values set according to the pre- and post-layout values . So, for example, a custom animation on "alpha" as the CHANGE_APPEARING animation will inherit the real value of alpha on the target object (presumably 1) as its starting and ending value when the animation begins . Animations which need to use values at the beginning and end that may not match the values queried when the transition begins may need to use a different mechanism than a standard ObjectAnimator object.
這麼大的兩段話,重點已被鄙人加粗。官方告訴我們, 對於任意動畫,起始和終止屬性值都是根據佈局前後的值來設定 。如果一個自定義CHANGE_APPEARING動畫是改變alpha值,那麼動畫開始時,它會繼承動畫目標的當前值作為起始和終止屬性值 —— 也就是說, 起始和終止值必須一樣 !
官方也說了,如果你定義的起始值、終止值和動畫開始時獲取的值不一樣,那麼預設的系統機制就支援不了了。自己想辦法吧。客觀上來講,這也是合理的,畢竟,這裡不是真的屬性變化動畫,View的狀態需要恢復到最初。
所以,rotation屬性的值設定為“0, 360”不生效,只有“0, 360, 0”才能正常工作,因為要回到初始狀態啊。不過,為什麼又一定要AnimatorSet呢?這一點,我也沒搞明白,看官您搞清楚的話,麻煩知會一聲啊!
按照上面的思路,再定義一個CHANGE_DISAPPEARING動畫,同樣是旋轉360度。
AnimatorSet animator3 = new AnimatorSet(); animator3.playTogether(ObjectAnimator.ofFloat(null, "rotation", 0, 360, 0)); layoutTransition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 800); layoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, animator3);

【注】 如果在DISAPPEARING或CHANGE_APPEARING還沒結束時,又來一個APPEARING動畫;或者是,在APPEARING或CHANGE_DISAPPEARING還沒結束時,又來一個DISAPPEARING,那本來的未結束動畫將停止,且View的狀態也會異常。具體現象就不貼出來了。
改變動畫
改變動畫即指CHANGE型別:由非增刪引起的佈局變化動畫。關於CHANGE動畫需要注意兩點:
- CHANGE動畫預設關閉,可呼叫 enableTransitionType(int) 方法開啟
- CHANGE動畫的作用目標是所有佈局發生改變的View,因為無增刪,所以動畫包括改變源本身
增加第三個目標容器,預設新增一個文字控制元件和圖片控制元件。文字控制元件的內容可改變,目的是模擬子View佈局變化。設定自定義動畫為透明度變化。
LayoutTransition layoutTransition = new LayoutTransition(); AnimatorSet animator = new AnimatorSet(); ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 1, 0.5f, 1); animator.play(anim); layoutTransition.setDuration(LayoutTransition.CHANGING, 1000); layoutTransition.setAnimator(LayoutTransition.CHANGING, animator); layoutTransition.enableTransitionType(LayoutTransition.CHANGING); mContainer3.setLayoutTransition(layoutTransition);
分別新增View及改變文字內容,觀察效果

可以看到,新增View是無法引起CHANGE動畫的,只有改變文字內容,從而使其佈局變化,才能讓所有的子View動起來。
小結
佈局動畫和佈局過渡在官方文件中,是雜糅到一塊來講的,鄙人並不苟同。雖然都是旨在“讓佈局變化動起來”,但是從使用方法來看,它們又截然不同。
本篇還留下部分疑問尚待解決。例如,為什麼CHANGE_APPEARING、CHANGE_DISAPPEARING和CHANGE的動畫,必須通過AnimationSet作為載體,才能成功實現?這個問題,估計需要閱讀原始碼才能解決了。
【附錄】

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。