Android屬性動(dòng)畫工作原理

什么是屬性動(dòng)畫

更改一個(gè)對象的屬性值時(shí),值的變化呈現(xiàn)動(dòng)畫效果。如一個(gè)Drawable的alpha值變化,或者一個(gè)Drawable在view上位置的變化。

屬性動(dòng)畫 vs 視圖動(dòng)畫

當(dāng)屬性動(dòng)畫作用于view的屬性時(shí),它呈現(xiàn)的效果和視圖動(dòng)畫相視,但它們還是有區(qū)別的:


屬性動(dòng)畫 vs 視圖動(dòng)畫.png

主要就是視圖動(dòng)畫僅針對View、Surface等視圖,而屬性動(dòng)畫不只針對視圖;視圖動(dòng)畫改變的僅僅是視覺效果,而屬性動(dòng)畫改變的是值本身,這也就是對一個(gè)view做如位移等動(dòng)畫時(shí),視圖動(dòng)畫結(jié)束后view的點(diǎn)擊區(qū)域還在原區(qū)域,而屬性動(dòng)畫view的點(diǎn)擊區(qū)域就在view呈現(xiàn)的位置。從包名上看也很明顯,屬性動(dòng)畫的包為android.animation,而視圖動(dòng)畫為android.view.animation。


動(dòng)畫分類.png

Animator

Animator是屬性動(dòng)畫的基類,給動(dòng)畫提供開始、結(jié)束、動(dòng)畫狀態(tài)回調(diào)等基礎(chǔ)操作。


Animator.png

它主要做的工作就是通過以下步驟來完成動(dòng)畫效果:

  1. 調(diào)用者提供需要做動(dòng)畫的屬性的初始值startValue和結(jié)束值endValue,及動(dòng)畫時(shí)長;
  2. 每隔固定的時(shí)間間隔返回這個(gè)屬性的一個(gè)值給調(diào)用者;
  3. 調(diào)用者把這個(gè)值設(shè)給動(dòng)畫對象的屬性;

如果在每個(gè)時(shí)間間隔到達(dá)時(shí)都通過同一個(gè)公式根據(jù)當(dāng)前動(dòng)畫的完成時(shí)長計(jì)算屬性值的話,我們獲得的動(dòng)畫效果肯定為線性的。
如:我們需要在40ms內(nèi)把一個(gè)view從x=0的位置移動(dòng)到x=40的位置,公式為f(v) = startValue + t * (endValue - startValue),其中 t = elapsedTime / totalTime線性效果如下:


google官方:animation-linear.png

但一般情況下,我們需要這個(gè)動(dòng)畫有一個(gè)變化節(jié)奏,如先慢后快再慢:


google官方:animation-nonlinear.png

這個(gè)時(shí)候我們需要每個(gè)時(shí)間間隔的計(jì)算公式返回的值是非線性的,這樣就需要這個(gè)公式有個(gè)變量,最簡單的就是根據(jù)當(dāng)前時(shí)間完成百分比來控制這個(gè)變量的值,如上面的非線性方程式可以為f(v) = startValue + f’(t) * (endValue - startValue),其中f'(t) = Math.cos(t+ 1) * Math.PI / 2.0f + 0.5f,t = elapsedTime / totalTime。在Animator模型里f(v)的計(jì)算封裝在TypeEvaluator類里,而f'(t)的計(jì)算封裝在TimeInpterpolator里,Animator只需在每個(gè)固定的時(shí)間間隔時(shí)傳入t(當(dāng)前時(shí)間點(diǎn),動(dòng)畫時(shí)間的完成百分比)即可。

TimeInterpolator

如上面的f'(t) = Math.cos(t+ 1) * Math.PI / 2.0f + 0.5f,t = elapsedTime / totalTime。TimeInterpolator提供了一個(gè)公式或匹配方式來獲取當(dāng)前動(dòng)畫完成時(shí)間百分比的插值,確保在計(jì)算屬性值時(shí)有一個(gè)變化率參數(shù)。它的輸入為當(dāng)前動(dòng)畫時(shí)長的完成百分比,輸出為插值。

Android提供的插值器有下面幾種,當(dāng)然你也可以自己定義:


時(shí)間插值器:TimeInterpolator.png

TypeEvaluator

如上面的f(v) = startValue + f’(t) * (endValue - startValue)。TypeEvaluator提供公式根據(jù)當(dāng)前TimeInterpolator提供的帶有變化率的動(dòng)畫時(shí)長完成百分比插值來計(jì)算屬性值。它的輸入為帶變化率的動(dòng)畫完成時(shí)長百分比,輸出為屬性值。

Android提供的TypeEvaluator有下面幾種(可自定義):


類型評估器:TypeEvaluator.png

TimeInterpolator 和 TypeEvalulator 的關(guān)系

TimeInterpolator & TypeEvaluator<T>.png

如何在固定的時(shí)間間隔開始屬性值計(jì)算?

上面我們提到的是每隔固定的時(shí)間間隔來獲取屬性值,那么怎么做到呢,或者可不可以根據(jù)有變化率的時(shí)間間隔來獲取值呢(這樣不需要TimeInterpolator)?
不管是drawable還是view,在動(dòng)畫更改某件屬性后,都需要更新顯示,所以我們最有效的方法是讓其和view的刷新在同一個(gè)脈沖里進(jìn)行。這樣我們很容易就想到根據(jù)顯示子系統(tǒng)的VSYNC脈沖來更新動(dòng)畫,實(shí)際上android的動(dòng)畫系統(tǒng)也是這么做的。Animator通過AnimationHandler向Choreographer注冊CALLBACK_ANIMATION來獲取固定脈沖即固定時(shí)間間隔,在收到脈沖時(shí)更新動(dòng)畫。

Choreographer從顯示子系統(tǒng)接收定時(shí)脈沖VSYN:


Choreographer.png

AnimationHandler向Choreographer注冊定時(shí)脈沖回調(diào):


AnimationHandler.png

因此我們總結(jié)下我們的思路:


思路.png

流程總結(jié)下來就是:


Animator流程.png

ValueAnimator

ValueAnimator就是實(shí)現(xiàn)Animator的一個(gè)類,它在動(dòng)畫時(shí)根據(jù)當(dāng)前脈沖時(shí)間計(jì)算動(dòng)畫屬性值,然后返回給動(dòng)畫調(diào)用方:


ValueAnimator實(shí)現(xiàn)Animator思路.png

其中最重要的就是根據(jù)當(dāng)前脈沖時(shí)間計(jì)算動(dòng)畫屬性值:


根據(jù)當(dāng)前幀時(shí)間計(jì)算動(dòng)屬性的動(dòng)畫值.png

其中TypeConverter是在TypeEvaluator計(jì)算出來的值和動(dòng)畫對象要設(shè)的屬性值類型不一致時(shí)使用,如動(dòng)畫對象的屬性值類型為PointF,而TypeEvaluator計(jì)算的值類型為float:


TypeConverter<T, V>.png

另外它實(shí)現(xiàn)了重復(fù)動(dòng)畫、反向動(dòng)畫、指定延時(shí)后開始動(dòng)畫的功能:


屬性動(dòng)畫可以定義的特征.png

具體的實(shí)現(xiàn)原理如下:


ValueAnimator.png

其中我們用到了PropertyValueHolder,它封裝了動(dòng)畫對象的屬性名和在動(dòng)畫過程中這個(gè)屬性的一組值。
我們首先看下Property<T, V>和Keyframe是指什么,Property<T, V>封裝了 屬性名/屬性值,而Keyframe封裝了 時(shí)間點(diǎn)/屬性值。


PropertyValuesHolder vs Property vs Keyframe vs Keyframes.png

再看看Property的實(shí)現(xiàn):


Property<T, V>.png

Keyframe的實(shí)現(xiàn):


Keyframe & Keyframes.png

PropertyValuesHolder的實(shí)現(xiàn):
PropertyValuesHolder.png

其實(shí)很簡單,就是保存了propertyName和Keyframes,要注意的點(diǎn)就是它獲取保存了這個(gè)屬性名的mSetter和mGetter反射方法,如我們對象View的屬性名propertyName = "alpha",它獲取保存了View#setAlpha(float alpha)和View#getAlpha()的反射Method,這在ObjectAnimator中會(huì)用到。

ObjectAnimator

根據(jù)上面ValueAnimator的內(nèi)容,我們在計(jì)算出屬性值后要返回給動(dòng)畫調(diào)用方讓它自己來重新設(shè)置對象的屬性值。那我們能不能直接幫忙把屬性值設(shè)給動(dòng)畫對象?
答案當(dāng)然是肯定,上面PropertyValuesHolder的實(shí)現(xiàn)原理時(shí)我們看到它獲取了動(dòng)畫屬性的setXXX()反射Method,我們完全可以在計(jì)算結(jié)束后用這個(gè)反射方法來給動(dòng)畫對象更新動(dòng)畫屬性值,只需給Animator多傳入一個(gè)動(dòng)畫對象。

ObjectAnimator vs ValueAnimator

ValueAnimator vs ObjectAnimator.png

原理

ObjectAnimator.png

由上面知道,ObjectAnimator的核心思想在于用反射方法來更新動(dòng)畫對象的屬性值,這要求我們的動(dòng)畫屬性在其類里必需有setXXX()的方法,如果這個(gè)set方法有參數(shù)的話還必需有g(shù)etXXX()方法。

ViewPropertyAnimator

如上面ValueAnimator和ObjectAnimator在給View的多個(gè)屬性做動(dòng)畫時(shí),每個(gè)屬性值更改時(shí)調(diào)用一次view.setXXX()方法,而每個(gè)setXXX()方法的實(shí)現(xiàn)里都調(diào)用了invalidate()一次,如果同時(shí)給多個(gè)View的屬性做動(dòng)畫的話,顯然性能不高,那么我們可不可以更新多個(gè)View的屬性值時(shí)只調(diào)用一次invalidate()呢?ViewPropertyAnimator就是通過直接調(diào)用RenderNode.setXXX()來更改屬性值,再統(tǒng)一調(diào)用一次invalidate()來使原布局失效。

ViewPropertyAnimator vs ObjectAnimator

ObjectAnimator vs ViewPropertyAnimator.png

通過上圖可以看到,ViewPropertAnimator在一次給多個(gè)View的屬性做動(dòng)畫時(shí)擁有更高優(yōu)的性能,同時(shí)它的調(diào)用方式也簡單多了。但它的動(dòng)畫對象僅僅是View。

其流程:


ViewPropertyAnimator流程.png

原理

ViewPropertyAnimator的實(shí)現(xiàn)和ValueAnimator不一樣的地方在于:

  1. 不需要直接調(diào)用start(),會(huì)根據(jù)View的Choreographer的脈沖回調(diào)自動(dòng)觸發(fā)。
  2. 并沒有通過ValueEvaluator計(jì)算值,而是直接寫死了計(jì)算值的公式。


    ViewPropertyAnimator原理.png

作用對象

那么ViewPropertyAnimator是不是實(shí)現(xiàn)了View中所有屬性的動(dòng)畫呢?并不是。其實(shí)現(xiàn)的動(dòng)畫屬性如下:


ViewPropertyAnimator中支持的View的屬性.png

TimeAnimator

如果我僅僅只想知道動(dòng)畫時(shí)長的消耗情況,不需要計(jì)算任何的屬性值,有沒有簡便的方法呢?TimeAnimator就提供了這個(gè)功能。


TimeAnimator.png

它提供一個(gè)回調(diào)函數(shù)返回:

  1. 上一幀到這一幀之間的時(shí)間增量;
  2. 動(dòng)畫啟動(dòng)以來的總時(shí)間。

AnimatorSet

AnimatorSet可以按指定順序(如,一起、按順序、延時(shí))播放一組Animator對象。

可以通過

  1. AnimatorSet#playTogether(Animator[])來指定一組Animator同時(shí)播放;
  2. AnimatorSet#playSequentially(Animator[])來指定一組Animator依次播放;
  3. Animator#play(Animator)和Builder類中的方法構(gòu)建一組Animator的播放順序。

如果AnimatorSet中的某些Animator的播放順序構(gòu)成了死循環(huán),如a1->a2->a3->a1,將表示這些動(dòng)畫將永遠(yuǎn)播放下去。

流程

其流程很簡單如下:


AnimatorSet流程圖.png

原理

其原理簡單來說就是:

  1. 根據(jù)動(dòng)畫是同時(shí)播放、還是先后播放構(gòu)建成一個(gè)關(guān)系圖,在A動(dòng)畫之前播放的動(dòng)畫是其parent,和A動(dòng)畫一起播放的是其sibling,在A動(dòng)畫之后播放的是其child。
  2. 定義一個(gè)時(shí)長為0的root動(dòng)畫,把沒有parent的動(dòng)畫設(shè)為其child。
  3. 通過parent的結(jié)束時(shí)間計(jì)算child的開始時(shí)間,再根據(jù)這個(gè)開始時(shí)間和child本身的動(dòng)畫總時(shí)長計(jì)算child的結(jié)束時(shí)間。
  4. 把每個(gè)動(dòng)畫和其開始時(shí)間、結(jié)束時(shí)間封裝成event,然后按照啟動(dòng)的先后順序加入到一個(gè)隊(duì)列中,每個(gè)時(shí)間脈沖到來時(shí),把這個(gè)脈沖之前需要執(zhí)行的event拿出來執(zhí)行。
    如下:


    AnimatorSet中Animator的關(guān)系圖.png

    總的實(shí)現(xiàn)原理:


    AnimatorSet.png

release版本的ObjectAnimator啟用失?。?/h1>

我們是否會(huì)碰到一個(gè)ObjectAnimation在debug版本中運(yùn)行的好好的,但是release版本就不行?
由上面ObjectAnimator的實(shí)現(xiàn)原理可以看到,我們在初始化時(shí)如果沒有傳入某個(gè)屬性的初始值,它會(huì)用反射方法去拿這個(gè)屬性的當(dāng)前值為其初始值;在屬性值計(jì)算完成后,也會(huì)用反射方法去把值設(shè)為對象的屬性值。當(dāng)release版本把這個(gè)動(dòng)畫對象的類混淆時(shí),反射方法找不到,于是動(dòng)畫無法正常運(yùn)行。

參考

Android官方:屬性動(dòng)畫
Android官方:ObjectAnimator
Android官方推薦博文:ViewPropertyAnimator
Android官方:View
material: elevation
material: elevation & shadow
Android官方:shadow
Android官方:animation-resource

原創(chuàng)文章,歡迎轉(zhuǎn)載,但請注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容