屬性動畫 ValueAnimator 執行原理全解析

請叫我大蘇發表於2018-03-31

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

最近下班時間都用來健身還有看書了,部落格被晾了一段時間了,原諒我~~~~

提問環節

好,廢話不多說,之前我們已經分析過 View 動畫 Animation 執行原理解析,那麼這次就來學習下屬性動畫的執行原理。

Q1:我們知道,Animation 動畫內部其實是通過 ViewRootImpl 來監聽下一個螢幕重新整理訊號,並且當接收到訊號時,從 DecorView 開始遍歷 View 樹的繪製過程中順帶將 View 繫結的動畫執行。那麼,屬性動畫(Animator)原理也是這樣麼?如果不是,那麼它又是怎麼實現的?

Q2:屬性動畫(Animator)區別於 Animation 動畫的就是它是有對 View 的屬性進行修改的,那麼它又是怎麼實現的,原理又是什麼?

Q3:屬性動畫(Animator)呼叫了 start() 之後做了些什麼呢?何時開始處理當前幀的動畫工作?內部又進行了哪些計算呢?

基礎

屬性動畫的使用,常接觸的其實就是兩個類 ValueAnimatorObjectAnimator。其實還有一個 View.animate(),這個內部原理也是屬性動畫,而且它已經將常用的動畫封裝好了,使用起來很方便,但會有一個坑,我們留著下一篇來介紹,本篇著重介紹屬性動畫的執行原理。

先看看基本的使用步驟:

//1.ValueAnimator用法  
ValueAnimator animator = ValueAnimator.ofInt(500);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
               int value = (int) animation.getAnimatedValue();
               mView.setX(value);  
         }
 });
animator.start();

//2.ObjectAnimator用法
ObjectAnimator animator = ObjectAnimator.ofInt(mView, "X", 500).setDuration(1000).start();
複製程式碼

這樣你就可以看到一個執行了 1s 的平移動畫,那麼接下去就該開始跟著原始碼走了,我們需要梳理清楚,這屬性動畫是什麼時候開始執行,如何執行的,真正生效的地方在哪裡,怎麼持續 1s 的,內部是如何進行計算的。

在之前分析 Animation 動畫執行原理後,我們也接著分析了 Android 螢幕重新整理機制,通過這兩篇,我們知道了 Android 螢幕重新整理的關鍵其實是 Choreographer 這個類,感興趣的可以再回去看看,這裡提幾點裡面的結論:

我們知道,Android 每隔 16.6ms 會重新整理一次螢幕,也就是每過 16.6ms 底層會發出一個螢幕重新整理訊號,當我們的 app 接收到這個螢幕重新整理訊號時,就會去計算螢幕資料,也就是我們常說的測量、佈局、繪製三大流程。這整個過程關鍵的一點是,app 需要先向底層註冊監聽下一個螢幕重新整理訊號事件,這樣當底層發出重新整理訊號時,才可以找到上層 app 並回撥它的方法來通知事件到達了,app 才可以接著去做計算螢幕資料之類的工作。

而註冊監聽以及提供回撥介面供底層呼叫的這些工作就都是由 Choreographer 來負責,Animation 動畫的原理是通過當前 View 樹的 ViewRootImpl 的 scheduleTraversals() 方法來實現,這個方法的內部邏輯會走到 Choreographer 裡去完成註冊監聽下一個螢幕重新整理訊號以及接收到事件之後的工作。

需要跟螢幕重新整理訊號打交道的話,歸根結底最後都是通過 Choreographer 這個類。

那麼,當我們在過屬性動畫(Animator)的流程原始碼時,我們就有一個初步的目標了,至少我們知道了需要跟蹤到 Choreographer 裡才可以停下來。至於屬性動畫的流程原理是跟 Animation 動畫流程一樣通過 ViewRootImpl 來實現的呢?還是其他的方式?這些就是我們這次過原始碼需要梳理出來的了,那麼下面就開始過原始碼吧。

原始碼解析

ps:本篇分析的原始碼基於 android-25 版本,版本不一樣,原始碼可能會有些差別,大夥自己過的時候注意一下。

過動畫原始碼的著手點應該都很簡單,跟著 start() 一步步追蹤下去梳理清楚就可以了。

我們知道 ObjectAnimator 是繼承的 ValueAnimator,那麼我們可以直接從 ValueAnimator 的 start() 開始看,等整個流程梳理清楚後,再回過頭看看 ObjectAnimator 的 start() 多做了哪些事就可以了:

ValueAnimator#start.png
很簡單,呼叫了內部的 start(boolean) 方法,

ValueAnimator#start(boolean).png
前面無外乎就是一些變數的初始化,然後好像呼叫了很多方法,emmm,其實我們並沒有必要每一行程式碼都去搞懂,我們主要是想梳理整個流程,那麼單看方法命名也知道,我們下面就跟著 startAnimation() 進去看看(但記得,如果後面跟不下去了,要回來這裡看看我們跳過的方法是不是漏掉了一些關鍵的資訊):

ValueAnimator#startAnimation.png
這裡呼叫了兩個方法,initAnimation()notifyStartListeners(),感覺這兩處也只是一些變數的初始化而已,還是沒涉及到流程的資訊啊,不管了,也還是先跟進去確認一下看看:

ValueAnimator#initAnimation.png
確實只是進行一些初始化工作而已,看看另外一個:

ValueAnimator#notifyStartListeners.png
這裡也只是通知動畫開始,回撥 listener 的介面而已。

emmm,我們從 start() 開始一路跟蹤下來,發現到目前為止都只是在做動畫的一些初始化工作而已,而且跟到這裡很明顯已經是盡頭了,下去沒有程式碼了,那麼動畫初始化之後的下一個步驟到底是在哪裡進行的呢?還記得我們前面在 start(boolean) 方法裡跳過了一些方法麼?也許關鍵就是在這裡,那麼再回頭過去看看:

ValueAnimator#start(boolean)2.png
我們剛才是根據方法命名,想當然的直接跟著 startAnimation() 走下去了,既然這條路走到底沒找到關鍵資訊,那麼就折回頭看看其他方法。這裡呼叫了 AnimationHandler 類的 addAnimationFrameCallback(),新出現了一個類,看命名應該是專門處理動畫相關的,而且還是單例類,跟進去看看:

AnimationHandler#addAnimationFrameCallback.png
首先第二個引數 delay 取決於我們是否呼叫了 setStartDelay() 來設定動畫的延遲執行,假設目前的動畫都沒有設定,那麼 delay 也就是 0,所以這裡著重看一下前面的程式碼。

mAnimationCallbacks 是一個 ArrayList,每一項儲存的是 AnimationFrameCallback 介面的物件,看命名這是一個回撥介面,那麼是誰在什麼時候會對它進行回撥呢?根據目前僅有的資訊,我們並沒有辦法看出來,那麼可以先放著,這裡只要記住第一個引數之前傳進來的是 this,也就是說如果這個介面被回撥時,那麼 ValueAnimator 對這個介面的實現將會被回撥。

接下去開始按順序過程式碼了,當 mAnimationCallbacks 列表大小等於 0 時,將會呼叫一個方法,很明顯,如果動畫是第一次執行的話,那麼這個列表大小應該就是 0,因為將 callback 物件新增到列表裡的操作是在這個判斷之後,所以這裡我們可以跟進看看:

AnimationHandler#getProvider.png

MyFrameCallbackProvider#postFrameCallback.png

哇,這麼快就看到 Choreographer 了,感覺我們好像已經快接近真相了,繼續跟下去:

Choreographer#postFrameCallback.png

Choreographer#postFrameCallbackDelayed.png

所以內部其實是呼叫了 postCallbackDelayedInternal() 方法,如果有看過我寫的上一篇部落格 Android 螢幕重新整理機制,到這裡是不是已經差不多可以理清了,有時間的可以回去看看,我這裡概括性地給出些結論。

Choreographer 內部有幾個佇列,上面方法的第一個引數 CALLBACK_ANIMATION 就是用於區分這些佇列的,而每個佇列裡可以存放 FrameCallback 物件,也可以存放 Runnable 物件。Animation 動畫原理上就是通過 ViewRootImpl 生成一個 doTraversal() 的 Runnable 物件(其實也就是遍歷 View 樹的工作)存放到 Choreographer 的佇列裡的。而這些佇列裡的工作,都是用於在接收到螢幕重新整理訊號時取出來執行的。但有一個關鍵點,Choreographer 要能夠接收到螢幕重新整理訊號的事件,是需要先呼叫 Choreographer 的 scheduleVsyncLocked() 方法來向底層註冊監聽下一個螢幕重新整理訊號事件的。

而如果繼續跟蹤 postCallbackDelayedInternal() 這個方法下去的話,你會發現,它最終就是走到了 scheduleVsyncLocked() 裡去,這些在上一篇部落格 Android 螢幕重新整理機制裡已經梳理過了,這裡就不詳細講了。

那麼,到這裡,我們就可以先來梳理一下目前的資訊了:

當 ValueAnimator 呼叫了 start() 方法之後,首先會對一些變數進行初始化工作並通知動畫開始了,然後 ValueAnimator 實現了 AnimationFrameCallback 介面,並通過 AnimationHander 將自身 this 作為引數傳到 mAnimationCallbacks 列表裡快取起來。而 AnimationHandler 在 mAnimationCallbacks 列表大小為 0 時會通過內部類 MyFrameCallbackProvider 將一個 mFrameCallback 工作快取到 Choreographer 的待執行佇列裡,並向底層註冊監聽下一個螢幕重新整理訊號事件。

當螢幕重新整理訊號到的時候,Choreographer 的 doFrame() 會去將這些待執行佇列裡的工作取出來執行,那麼此時也就回撥了 AnimationHandler 的 mFrameCallback 工作。

那麼到目前為止,我們能夠確定,當動畫第一次呼叫 start(),這裡的第一次應該是指專案裡所有的屬性動畫裡某個動畫第一次呼叫 start(),因為 AnimationHandler 是一個單例類,顯然是為所有的屬性動畫服務的。如果是第一次呼叫了 start(),那麼就會去向底層註冊監聽下一個螢幕重新整理訊號的事件。所以動畫的處理邏輯應該就是在接收到螢幕重新整理訊號之後回撥到的 mFrameCallback 工作裡會去間接的呼叫到的了。

那麼,接下去就繼續看看,當接收到螢幕重新整理訊號之後,mFrameCallback 又繼續做了什麼

AnimationHandler#mFrameCallback.png

其實就做了兩件事,一件是去處理動畫的相關工作,也就是說要找到動畫真正執行的地方,跟著 doAnimationFrame() 往下走應該就行了。而剩下的程式碼就是處理另外一件事:繼續向底層註冊監聽下一個螢幕重新整理訊號。

先講講第二件事,我們知道,動畫是一個持續的過程,也就是說,每一幀都應該處理一個動畫進度,直到動畫結束。既然這樣,我們就需要在動畫結束之前的每一個螢幕重新整理訊號都能夠接收到,所以在每一幀裡都需要再去向底層註冊監聽下一個螢幕重新整理訊號事件。所以你會發現,上面程式碼裡引數是 this,也就是 mFrameCallback 本身,結合一下之前的那個流程,這裡可以得到的資訊是:

當第一個屬性動畫呼叫了 start() 時,由於 mAnimationCallbacks 列表此時大小為 0,所以直接由 addAnimationFrameCallback() 方法內部間接的向底層註冊下一個螢幕重新整理訊號事件,然後將該動畫加入到列表裡。而當接收到螢幕重新整理訊號時,mFrameCallback 的 doFrame() 會被回撥,該方法內部做了兩件事,一是去處理當前幀的動畫,二則是根據列表的大小是否不為 0 來決定繼續向底層註冊監聽下一個螢幕重新整理訊號事件,如此反覆,直至列表大小為 0。

所以,這裡可以猜測一點,如果當前動畫結束了,那麼就需要將其從 mAnimationCallbacks 列表中移除,這點可以後面跟原始碼過程中來驗證。
那麼,下去就是跟著 doAnimationFrame() 來看看,屬性動畫是怎麼執行的:

AnimationHandler#doAnimationFrame.png
這裡概括下其實就做了兩件事:

一是去迴圈遍歷列表,取出每一個 ValueAnimator,然後判斷動畫是否有設定了延遲開始,或者說動畫是否到時間該執行了,如果到時間執行了,那麼就會去呼叫 ValueAnimator 的 doAnimationFrame()

二是呼叫了 cleanUpList() 方法,看命名就可以猜測是去清理列表,那麼應該也就是處理掉已經結束的動畫,因為 AnimationHandler 是為所有屬性動畫服務的,同一時刻也許有多個動畫正在進行中,那麼動畫的結束肯定有先後,已經結束的動畫肯定要從列表中移除,這樣等所有動畫都結束了,列表大小變成 0 了,mFrameCallback 才可以停止向底層註冊監聽下一個螢幕重新整理訊號事件,AnimationHandler 才可以進入空閒狀態,不用再每一幀都去處理動畫的工作。

那麼,我們優先看看 cleanUpList(),因為感覺它的工作比較簡單,那就先梳理掉:

AnimationHandler#cleanUpList.png
猜測正確,將列表中為 null 的物件都移除掉,那麼我們就可以繼續進一步猜測,動畫如果結束的話,會將自身在這個列表中的引用賦值為 null,這點可以在稍微跟蹤動畫的流程中來進行確認。

清理的工作梳理完,那麼接下去就是繼續去跟著動畫的流程了,還記得我們上面提到了另一件事是遍歷列表去呼叫每個動畫 ValueAnimator 的 doAnimationFrame() 來處理動畫邏輯麼,那麼我們接下去就跟進這個方法看看:

ValueAnimator#doAnimationFrame.png
上面省略了部分程式碼,省略的那些程式碼跟動畫是否被暫停或重新開始有關,本篇優先梳理正常的動畫流程,這些就先不關注了。

稍微概括一下,這個方法內部其實就做了三件事:
一是處理第一幀動畫的一些工作;

二是根據當前時間計算當前幀的動畫進度,所以動畫的核心應該就是在 animateBaseOnTime() 這個方法裡,意義就類似 Animation 動畫的 getTransformation()方法;

三是判斷動畫是否已經結束了,結束了就去呼叫 endAnimation(),按照我們之前的猜測,這個方法內應該就是將當前動畫從 mAniamtionCallbacks 列表裡移除。

我們先來看動畫結束之後的處理工作,因為上面才剛梳理了一部分,趁著現在大夥還有些印象,而且這部分工作會簡單易懂點,先把簡單的吃掉:

ValueAnimator#endAnimation.png

很簡單,兩件事,一是去通知說動畫結束了,二是呼叫了 AniamtionHandler 的 removeCallback(),繼續跟進看看:

AnimationHandler#removeCallback.png
我們之前的猜測在這裡得到驗證了吧,如果動畫結束,那麼它會將其自身在 AnimationCallbacks 列表裡的引用賦值為 null,然後移出列表的工作就交由 AnimationHandler 去做。我們說了,AnimationHandler 是為所有的屬性動畫服務的,那麼當某個動畫結束的話,就必須進行一些資源清理的工作,整個清理的流程邏輯就是我們目前梳理出來的這樣。

好,搞定了一個小點了,那麼接下去繼續看剩下的兩件事,先看第一件,處理動畫第一幀的工作問題

參考 Animation 動畫的原理,第一幀的工作通常都是為了記錄動畫第一幀的時間戳,因為後續的每一幀裡都需要根據當前時間以及動畫第一幀的時間還有一個動畫持續時長來計算當前幀動畫所處的進度,Animation 動畫我們梳理過了,所以這裡在過第一幀的邏輯時應該就會比較有條理點。我們來看看,屬性動畫的第一幀的工作是不是跟 Animation 差不多:

ValueAnimator#doAnimationFrame2.png

emmm,看來比 Animation 動畫複雜多了,大體上也是幹了兩件事:

一是呼叫了 AnimationHandler 的 addOneShotCommitCallback() 方法,具體是幹嘛的我們等會來分析;

二就是記錄動畫第一幀的時間了,mStartTime 變數就是表示第一幀的時間戳,後續的動畫進度計算肯定需要用到這個變數。至於還有一個 mSeekFraction 變數,它的作用有點類似於我們用電腦看視訊時,可以任意選擇從某個進度開始觀看。屬性動畫有提供了一個介面 setCurrentPlayTime()

ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(4000);
animator.start();
複製程式碼

舉個例子,。這是一個持續 4s 從 0 增長到 100 的動畫,如果我們呼叫了 start(),那麼 mSeekFraction 預設值是 -1,所以 mStartTime 就是用當前時間作為動畫的第一幀時間。如果我們呼叫了 setCurrentPlayTime(2000),意思就是說,我們希望這個動畫從 2s 開始,那麼它就是一個持續 2s(4-2) 的從 50 增長到 100 的動畫(假設插值器為線性),所以這個時候,mStartTime 就是以比當前時間還早 2s 作為動畫的第一幀時間,後面根據 mStartTime 計算動畫進度時,就會發現原來動畫已經過了 2s 了。

就像我們看電視時,我們不想看片頭,所以直接選擇從正片開始看,類似的道理。

好了,還記得前面說了處理動畫第一幀的工作大體上有兩件事,另一件是呼叫了一個方法麼。我們回頭來看看,這裡又是做了些什麼:

AnimationHandler#addOneShotCommitCallback.png

只是將 ValueAnimator 新增到 AnimationHandler 裡的另一個列表中去,可以過濾這個列表的變數名看看它都在哪些地方被使用到了:

AnimationHandler#doAnimationFrame2.png
這地方還記得吧,我們上面分析的那一大堆工作都是跟著 callback.doAnimationFrame(frameTime) 這行程式碼走進去的,雖然內部做的事我們還沒全部分析完,但我們這裡可以知道,等內部所有事都完成後,會退回到 AnimationHandler 的 doAnimationFrame() 繼續往下幹活,所以再繼續跟下去看看:

AnimationHandler#postCommitCallback.png
上面說過,Choreographer 內部有多個佇列,每個佇列裡都可以存放 FrameCallback 物件,或者 Runnable 物件。這次是傳到了另一個佇列裡,傳進的是一個 Runnable 物件,我們看看這個 Runnable 做了些什麼:

AnimationHandler#commitAnimationFrame.png

ValueAnimator 實現了 AnimationFrameCallback 介面,這裡等於是回撥了 ValueAnimator 的方法,然後將其從佇列中移除。看看 ValueAnimator 的實現做了些什麼:

ValueAnimator#commitAnimationFrame.png

好嘛,這裡說穿了其實也是在修正動畫的第一幀時間 mStartTime。那麼,其實也就是說,ValueAnimator 的 doAnimationFrame() 裡處理第一幀工作的兩件事全部都是用於計算動畫的第一幀時間,只是一件是根據是否 "跳過片頭"( setCurrentPlayTime()) 計算,另一件則是這裡的修正。

那麼,這裡為什麼要對第一幀時間 mStartTime 進行修正呢?

大夥有時間可以去看看 AnimationFrameCallback 介面的 commitAnimationFrame() 方法註釋,官方解釋得特別清楚了,我這裡就不貼圖了,直接將我的理解寫出來:

其實,這跟屬性動畫通過 Choreographer 的實現原理有關。我們知道,螢幕的重新整理訊號事件都是由 Choreographer 負責,它內部有多個佇列,這些佇列裡存放的工作都是用於在接收到訊號時取出來處理。那麼,這些佇列有什麼區別呢?

其實也就是執行的先後順序的區別,按照執行的先後順序,我們假設這些佇列的命名為:1佇列 > 2佇列 > 3佇列。我們本篇分析的屬性動畫,AnimationHandler 封裝的 mFrameCallback 工作就是放到 1佇列裡的;而之前分析的 Animation 動畫,它通過 ViewRootImpl 封裝的 doTraversal() 工作是放到 2佇列裡的;而上面剛過完的修正動畫第一幀時間的 Runnable 工作則是放到 3佇列裡的。

也就是說,當接收到螢幕重新整理訊號後,屬性動畫會最先被處理。然後是去計算當前螢幕資料,也就是測量、佈局、繪製三大流程。但是這樣會有一個問題,如果頁面太過複雜,繪製當前介面時花費了太多的時間,那麼等到下一個螢幕重新整理訊號時,屬性動畫根據之前記錄的第一幀時間戳計算動畫進度時,會發現丟了開頭的好幾幀,明明動畫沒還執行過。所以,這就是為什麼需要對動畫第一幀時間進行修正。

當然,如果動畫已經開始了,在動畫中間某一幀,就不會去修正了,這個修正,只是針對動畫的第一幀時間。因為,如果是在第一幀發現繪製介面太耗時,丟了開頭幾幀,那麼我們可以通過延後動畫開始的時機來達到避免丟幀。但如果是在動畫執行過程中才遇到繪製介面太耗時,那不管什麼策略都無法避免丟幀了。


小結1:

好了,到這裡,大夥先休息下,我們來梳理一下目前所有的資訊,不然我估計大夥已經忘了上面講過什麼了:

  1. ValueAnimator 屬性動畫呼叫了 start() 之後,會先去進行一些初始化工作,包括變數的初始化、通知動畫開始事件;

  2. 然後通過 AnimationHandler 將其自身 this 新增到 mAnimationCallbacks 佇列裡,AnimationHandller 是一個單例類,為所有的屬性動畫服務,列表裡存放著所有正在進行或準備開始的屬性動畫;

  3. 如果當前存在要執行的動畫,那麼 AnimationHandler 會去通過 Choreographer 向底層註冊監聽下一個螢幕重新整理訊號,當接收到訊號時,它的 mFrameCallback 會開始進行工作,工作的內容包括遍歷列表來分別處理每個屬性動畫在當前幀的行為,處理完列表中的所有動畫後,如果列表還不為 0,那麼它又會通過 Choreographer 再去向底層註冊監聽下一個螢幕重新整理訊號事件,如此反覆,直至所有的動畫都結束。

  4. AnimationHandler 遍歷列表處理動畫是在 doAnimationFrame() 中進行,而具體每個動畫的處理邏輯則是在各自,也就是 ValueAnimator 的 doAnimationFrame() 中進行,各個動畫如果處理完自身的工作後發現動畫已經結束了,那麼會將其在列表中的引用賦值為空,AnimationHandler 最後會去將列表中所有為 null 的都移除掉,來清理資源。

  5. 每個動畫 ValueAnimator 在處理自身的動畫行為時,首先,如果當前是動畫的第一幀,那麼會根據是否有"跳過片頭"(setCurrentPlayTime())來記錄當前動畫第一幀的時間 mStartTime 應該是什麼。

  6. 第一幀的動畫其實也就是記錄 mStartTime 的時間以及一些變數的初始化而已,動畫進度仍然是 0,所以下一幀才是動畫開始的關鍵,但由於屬性動畫的處理工作是在繪製介面之前的,那麼有可能因為繪製耗時,而導致 mStartTime 記錄的第一幀時間與第二幀之間隔得太久,造成丟了開頭的多幀,所以如果是這種情況下,會進行 mStartTime 的修正。

  7. 修正的具體做法則是當繪製工作完成後,此時,再根據當前時間與 mStartTime 記錄的時間做比較,然後進行修正。

  8. 如果是在動畫過程中的某一幀才出現繪製耗時現象,那麼,只能表示無能為力了,丟幀是避免不了的了,想要解決就得自己去分析下為什麼繪製會耗時;而如果是在第一幀是出現繪製耗時,那麼,系統還是可以幫忙補救一下,修正下 mStartTime 來達到避免丟幀。

好了,休息結束,我們繼續,還有一段路要走,其實整個流程目前大體上已經出來了,只是缺少了當前幀的動畫進度具體計算實現細節,這部分估計會更讓人頭大。

之前分析 ValueAnimator 的 doAnimationFrame() 時,我們將其概括出來主要做了三件事:一是處理第一幀動畫的工作;二是根據當前時間計算並實現當年幀的動畫工作;三是根據動畫是否結束進行一些資源清理工作;一三我們都分析了,下面就來過過第二件事,animateBasedOnTime()

ValueAnimator#animateBaseOnTime.png

從這裡開始,就是在計算當前幀的動畫邏輯了,整個過程跟 Animation 動畫基本上差不多。上面的程式碼裡,我省略了一部分,那部分是用於根據是否設定的 mRepeatCount 來處理動畫結束後是否需要重新開始,這些我們就不看了,我們著重梳理一個正常的流程下來即可。

所以,概括一下,這個方法裡其實也就是做了三件事:

一是,根據當前時間以及動畫第一幀時間還有動畫持續的時長來計算當前的動畫進度。

二是,確保這個動畫進度的取值在 0-1 之間,這裡呼叫了兩個方法來輔助計算,我們就不跟進去了,之所以有這麼多的輔助計算,那是因為,屬性動畫支援 setRepeatCount() 來設定動畫的迴圈次數,而從始至終的動畫第一幀的時間都是 mStrtTime 一個值,所以在第一個步驟中根據當前時間計算動畫進度時會發現進度值是可能會超過 1 的,比如 1.5, 2.5, 3.5 等等,所以第二個步驟的輔助計算,就是將這些值等價換算到 0-1 之間。

三就是最重要的了,當前幀的動畫進度計算完畢之後,就是需要應用到動畫效果上面了,所以 animateValue() 方法的意義就是類似於 Animation 動畫中的 applyTransformation()

我們都說,屬性動畫是通過修改屬性值來達到動畫效果的,那麼我們就跟著 animateValue() 進去看看:

ValueAnimator#animateValue.png

這裡乾的活我也大概的給劃分成了三件事:

一是,根據插值器來計算當前的真正的動畫進度,插值器算是動畫裡比較重要的一個概念了,可能平時用的少,如果我們沒有明確指定使用哪個插值器,那麼系統通常會有一個預設的插值器。

二是,根據插值器計算得到的實際動畫進度值,來對映到我們需要的數值。這麼說吧,就算經過了插值器計算之後,動畫進度值也只是 0-1 區間內的某個值而已。而我們通常需要的並不是 0-1 的數值,比如我們希望一個 0-500 的變化,那麼我們就需要自己在拿到 0-1 區間的進度值後來進行轉換。第二個步驟,大體上的工作就是幫助我們處理這個工作,我們只需要告訴 ValueAnimator 我們需要 0-500 的變化,那麼它在拿到進度值後會進行轉換。

三就只是通知動畫的進度回撥而已了。

流程上差不多已經梳理出來了,不過我個人對於內部是如何根據拿到的 0-1 區間的進度值轉換成我們指定區間的數值的工作挺感興趣的,那麼我們就稍微再深入去分析一下好了。這部分工作主要就是呼叫了 mValues[i].calculateValue(fraction) 這一行程式碼來實現,mValues 是一個 PropertyValuesHolder 型別的陣列,所以關鍵就是去看看這個類的 calculateValue() 做了啥:

PropertyValuesHolder#calculateValue.png

我們在使用 ValueAnimator 時,註冊了動畫進度回撥,然後在回撥裡取當前的值時其實也就是取到上面那個 mAnimatedValue 變數的值,而這個變數的值是通過 mKeyframes.getValue() 計算出來的,那麼再繼續跟進看看:

KeyFrames#getValue.png

KeyFrames 是一個介面,那麼接下去就是要找找哪裡實現了這個介面:

PropertyValuesHolder#setIntValues.png

具體的找法,可以在 PropertyValuesHolder 這個類裡利用 Ctrl + F 過濾一下 mKeyframes =來看一下它在哪些地方被例項化了。匹配到的地方很多,但都差不多,都是通過 KeyframeSet 的 ofXXX 方法例項化得到的物件,那麼具體的實現應該就是在 KeyframeSet 這個類裡了。

在跟進去看之前,有一點想提一下,大夥應該注意到了吧,mKeyframes 例項化的這些地方,ofInt()onFloat() 等等是不是很熟悉。沒錯,就是我們建立屬性動畫時相似的方法名, 其實 ValueAnimator.ofInt() 內部會根據相應的方法來建立 mKeyframes 物件,也就是說,在例項化屬性動畫時,這些 mKeyframes 也順便被例項化了。想確認的,大夥可以自己去跟下原始碼看看,我這裡就不貼了。

好了,接下去看看 KeyframeSet 這個類的 ofInt() 方法,看看它內部具體是建立了什麼:

KeyframeSet#ofInt.png

這裡又涉及到新的機制了吧,Keyframe,KeyframeSet,Keyframes 這些大夥感興趣可以去查檢視,我也沒有深入去了解。但看了別人的一些介紹,這裡大概講一下。直接從翻譯上來看,這個也就是指關鍵幀,就像一部電影由多幀畫面組成一樣的道理,動畫也是由一幀幀組成的。

還記得,我們為啥會跟到這裡來了麼。動畫在處理當前幀的工作時,會去計算當前幀的動畫進度,然後根據這個 0-1 區間的進度,對映到我們需要的數值,而這個對映之後的數值就是通過 mKeyframes 的 getValue() 裡取到的,mKeyframes 是一個 KeyframeSet 物件,在建立屬性動畫時也順帶被建立了,而建立屬性動畫時,我們會傳入一個我們想要的數值,如 ValueAnimator.ofInt(100) 就表示我們想要的動畫變化範圍是 0-100,那麼這個 100 在內部也會被傳給 KeyframeSet.ofInt(100),然後就是進入到上面程式碼塊裡的建立工作了。

在這個方法裡,100 就是作為一個關鍵幀。那麼,對於一個動畫來說,什麼才叫做關鍵幀呢?很明顯,至少動畫需要知道從哪開始,到哪結束,是吧?所以,對於一個動畫來說,至少需要兩個關鍵幀,如果我們呼叫 ofInt(100) 只傳進來一個數值時,那麼內部它就預設認為起點是從 0 開始,傳進來的 100 就是結束的關鍵幀,所以內部就會自己建立了兩個關鍵幀。

那麼,這些關鍵幀又是怎麼被動畫用上的呢?這就是回到我們最初跟蹤的 mKeyframes.getValue() 這個方法裡去了,看上面的程式碼塊,KeyframeSet.ofInt() 最後是建立了一個 IntKeyframeSet 物件,所以我們跟進這個類的 getValue() 方法裡看看它是怎麼使用這些關鍵幀的:

IntKeyframeSet#getValue.png

所以關鍵的工作就都在 getIntValue() 這裡了,引數傳進來還記得是什麼吧,就是經過插值器計算之後當前幀的動畫進度值,0-1 區間的那個值,getIntValue() 這個方法的程式碼有些多,我們一塊一塊來看,先看第一塊:

IntKeyframeSet#getIntValue.png

當關鍵幀只有兩幀時,我們常使用的 ValueAnimator.ofInt(100), 內部其實就是隻建立了兩個關鍵幀,一個是起點 0,一個是結束點 100。那麼,在這種只有兩幀的情況下,將 0-1 的動畫進度值轉換成我們需要的 0-100 區間內的值,系統的處理很簡單,如果沒有設定估值器,也就是 mEvaluator,那麼就直接是按比例來轉換,比如進度為 0.5,那按比例轉換就是 (100 - 0) * 0.5 = 50。如果有設定估值器,那就按照估值器定的規則來,估值器其實就是類似於插值器,屬性動畫裡才引入的概念,Animation 動畫並沒有,因為只有屬性動畫內部才幫我們做了值轉換工作。

上面是當關鍵幀只有兩幀時的處理邏輯,那麼當關鍵幀超過兩幀的時候呢:

IntKeyframeSet#getIntValue2.png
當關鍵幀超過兩幀時,分三種情況來處理:第一幀的處理;最後一幀的處理;中間幀的處理;

那麼,什麼時候關鍵幀會超過兩幀呢?其實也就是我們這麼使用的時候:ValueAnimator.ofInt(0, 100, 0, -100, 0),類似這種用法的時候關鍵幀就不止兩個了,這時候數量就是根據引數的個數來決定的了。

那麼,我們再來詳細看看三種情況的處理邏輯,首先是第一幀的處理邏輯:

IntKeyframeSet#getIntValue3.png

fraction <= 0f 表示的應該不止是第一幀的意思,但除了理解成第一幀外,我不清楚其他場景是什麼,暫時以第一幀來理解,這個應該影響不大。

處理的邏輯其實也很簡單,還記得當只有兩個關鍵幀時是怎麼處理的吧。那在處理第一幀的工作時,只需要將第二幀當成是最後一幀,那麼第一幀和第二幀這樣也就可以看成是隻有兩幀的場景了吧。但是引數 fraction 動畫進度是以實際第一幀到最後一幀計算出來的,所以需要先對它進行轉換,換算出它在第一幀到第二幀之間的進度,接下去的邏輯也就跟處理兩幀時的邏輯是一樣的了。

同樣的道理,在處理最後一幀時,只需要取出倒數第一幀跟倒數第二幀的資訊,然後將進度換算到這兩針之間的進度,接下去的處理邏輯也就是一樣的了。程式碼我就不貼了。

但處理中間幀的邏輯就不一樣了,因為根據 0-1 的動畫進度,我們可以很容易區分是處於第一幀還是最後一幀,無非一個就是 0,一個是 1。但是,當動畫進度值在 0-1 之間時,我們並沒有辦法直接看出這個進度值是落在中間的哪兩個關鍵幀之間,如果有辦法計算出當前的動畫進度處於哪兩個關鍵幀之間,那麼接下去的邏輯也就是一樣的了,所以關鍵就是在於找出當前進度處於哪兩個關鍵幀之間:

IntKeyframeSet#getIntValue4.png

系統的找法也很簡單,從第一幀開始,按順序遍歷每一幀,然後去判斷當前的動畫進度跟這一幀儲存的位置資訊來找出當前進度是否就是落在某兩個關鍵幀之間。因為每個關鍵幀儲存的資訊除了有它對應的值之外,還有一個是它在第一幀到最後一幀之間的哪個位置,至於這個位置的取值是什麼,這就是由在建立這一系列關鍵幀時來控制的了。

還記得是在哪裡建立了這一系列的關鍵幀的吧,回去 KeyframeSet 的 ofInt() 裡看看:

KeyframeSet#ofInt2.png

在建立每個關鍵幀時,傳入了兩個引數,第一個引數就是表示這個關鍵幀在整個區域之間的位置,第二引數就是它表示的值是多少。看上面的程式碼, i 表示的是第幾幀,numKeyframes 表示的是關鍵幀的總數量,所以 i/(numKeyframes - 1) 也就是表示這一系列關鍵幀是按等比例來分配的。

比如說, ValueAnimator.ofInt(0, 50, 100, 200),這總共有四個關鍵幀,那麼按等比例分配,第一幀就是在起點位置 0,第二幀在 1/3 位置,第三幀在 2/3 的位置,最後一幀就是在 1 的位置。


小結2:

到這裡,我們再來梳理一下後面部分過的內容:

  1. 當接收到螢幕重新整理訊號後,AnimationHandler 會去遍歷列表,將所有待執行的屬性動畫都取出來去計算當前幀的動畫行為。

  2. 每個動畫在處理當前幀的動畫邏輯時,首先會先根據當前時間和動畫第一幀時間以及動畫的持續時長來初步計算出當前幀時動畫所處的進度,然後會將這個進度值等價轉換到 0-1 區間之內。

  3. 接著,插值器會將這個經過初步計算之後的進度值根據設定的規則計算出實際的動畫進度值,取值也是在 0-1 區間內。

  4. 計算出當前幀動畫的實際進度之後,會將這個進度值交給關鍵幀機制,來換算出我們需要的值,比如 ValueAnimator.ofInt(0, 100) 表示我們需要的值變化範圍是從 0-100,那麼插值器計算出的進度值是 0-1 之間的,接下去就需要藉助關鍵幀機制來對映到 0-100 之間。

  5. 關鍵幀的數量是由 ValueAnimator.ofInt(0, 1, 2, 3) 引數的數量來決定的,比如這個就有四個關鍵幀,第一幀和最後一幀是必須的,所以最少會有兩個關鍵幀,如果引數只有一個,那麼第一幀預設為 0,最後一幀就是引數的值。當呼叫了這個 ofInt() 方法時,關鍵幀組也就被建立了。

  6. 當只有兩個關鍵幀時,對映的規則是,如果沒有設定估值器,那麼就等比例對映,比如動畫進度為 0.5,需要的值變化區間是 0-100,那麼等比例對映後的值就是 50,那麼我們在 onAnimationUpdate 的回撥中通過 animation.getAnimatedValue() 獲取到的值 50 就是這麼來的。

  7. 如果有設定估值器,那麼就按估值器的規則來進行對映。

  8. 當關鍵幀超過兩個時,需要先找到當前動畫進度是落於哪兩個關鍵幀之間,然後將這個進度值先對映到這兩個關鍵幀之間的取值,接著就可以將這兩個關鍵幀看成是第一幀和最後一幀,那麼就可以按照只有兩個關鍵幀的情況下的對映規則來進行計算了。

  9. 而進度值對映到兩個關鍵幀之間的取值,這就需要知道每個關鍵幀在整個關鍵幀組中的位置資訊,或者說權重。而這個位置資訊是在建立每個關鍵幀時就傳進來的。onInt() 的規則是所有關鍵幀按等比例來分配權重,比如有三個關鍵幀,第一幀是 0,那麼第二幀就是 0.5, 最後一幀 1。

至此,我們已經將整個流程梳理出來了,兩部分小結的內容整合起來就是這次梳理出來的整個屬性動畫從 start() 之後,到我們在 onAnimationUpdate 回撥中取到我們需要的值,再到動畫結束後如何清理資源的整個過程中的原理解析。

梳理清楚後,大夥應該就要清楚,屬性動畫是如何接收到螢幕重新整理訊號事件的?是如何反覆接收到螢幕重新整理訊號事件直到整個動畫執行結束?方式是否是有區別於 Animation 動畫的?計算當前幀的動畫工作都包括了哪些?是如何將 0-1 的動畫進度對映到我們需要的值上面的?

如果看完本篇,這些問題你心裡都有譜了,那麼就說明,本篇的主要內容你都吸收進去了。當然,如果有錯的地方,歡迎指出來,畢竟內容確實很多,很有可能存在寫錯的地方沒發現。

來張時序圖結尾:

VauleAnimatior執行原理時序圖.png

最後,有一點想提的是,我們本篇只是過完了 ValueAnimator 的整個流程原理,但這整個過程中,注意到了沒有,我們並沒有看到有任何一個地方涉及到了 ui 操作。在上一篇部落格 ***Android 螢幕重新整理機制***中,我們也清楚了,介面的繪製其實就是交由 ViewRootImpl 來發起的,但很顯然,ValueAnimator 跟 ViewRootImpl 並沒有任何交集。

那麼,ValueAnimator 又是怎麼實現動畫效果的呢?其實,ValueAnimator 只是按照我們設定的變化區間(ofInt(0, 100)),持續時長(setDuration(1000)),插值器規則,估值器規則,內部在每一幀內通過一系列計算,轉換等工作,最後輸出每一幀一個數值而已。而如果要實現一個動畫效果,那麼我們只能在進度回撥介面取到這個輸出的值,然後手動應用到某個 View 上面(mView.setX())。所以,這種使用方式,本質上仍然是通過 View 的內部方法最終走到 ViewRootImpl 去觸發介面的更新繪製。

而 ObjectAnimator 卻又不同了,它內部就有涉及到 ui 的操作,具體原理是什麼,留待後續再分析。

遺留問題

都說屬性動畫是通過改變屬性值來達到動畫效果的,計劃寫這一篇時,本來以為可以梳理清楚這點的,誰知道單單只是把 ValueAnimator 的流程原理梳理出來篇幅就這麼長了,所以 ObjectAnimator 就另找時間再來梳理吧,這個問題就作為遺留問題了。

Q1:都說屬性動畫是通過改變屬性值來達到動畫效果的,那麼它的原理是什麼呢?


QQ圖片20180316094923.jpg
最近(2018.03)剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支援~~

相關文章