本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android
最近下班時間都用來健身還有看書了,博客被晾了一段時間了,原諒我~~~~數組
好,廢話很少說,以前咱們已經分析過 View 動畫 Animation 運行原理解析,那麼此次就來學習下屬性動畫的運行原理。緩存
Q1:咱們知道,Animation 動畫內部實際上是經過 ViewRootImpl 來監聽下一個屏幕刷新信號,而且當接收到信號時,從 DecorView 開始遍歷 View 樹的繪製過程當中順帶將 View 綁定的動畫執行。那麼,屬性動畫(Animator)原理也是這樣麼?若是不是,那麼它又是怎麼實現的?微信
Q2:屬性動畫(Animator)區別於 Animation 動畫的就是它是有對 View 的屬性進行修改的,那麼它又是怎麼實現的,原理又是什麼?app
Q3:屬性動畫(Animator)調用了 start()
以後作了些什麼呢?什麼時候開始處理當前幀的動畫工做?內部又進行了哪些計算呢?ide
屬性動畫的使用,常接觸的其實就是兩個類 ValueAnimator, ObjectAnimator。其實還有一個 View.animate(),這個內部原理也是屬性動畫,並且它已經將經常使用的動畫封裝好了,使用起來很方便,但會有一個坑,咱們留着下一篇來介紹,本篇着重介紹屬性動畫的運行原理。佈局
先看看基本的使用步驟:post
//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()
多作了哪些事就能夠了:
很簡單,調用了內部的
start(boolean)
方法,
前面無外乎就是一些變量的初始化,而後好像調用了不少方法,emmm,其實咱們並無必要每一行代碼都去搞懂,咱們主要是想梳理整個流程,那麼單看方法命名也知道,咱們下面就跟着
startAnimation()
進去看看(但記得,若是後面跟不下去了,要回來這裏看看咱們跳過的方法是否是漏掉了一些關鍵的信息):
這裏調用了兩個方法,
initAnimation()
和 notifyStartListeners()
,感受這兩處也只是一些變量的初始化而已,仍是沒涉及到流程的信息啊,無論了,也仍是先跟進去確認一下看看:
確實只是進行一些初始化工做而已,看看另一個:
這裏也只是通知動畫開始,回調 listener 的接口而已。
emmm,咱們從 start()
開始一路跟蹤下來,發現到目前爲止都只是在作動畫的一些初始化工做而已,並且跟到這裏很明顯已是盡頭了,下去沒有代碼了,那麼動畫初始化以後的下一個步驟究竟是在哪裏進行的呢?還記得咱們前面在 start(boolean)
方法裏跳過了一些方法麼?也許關鍵就是在這裏,那麼再回頭過去看看:
咱們剛纔是根據方法命名,想固然的直接跟着
startAnimation()
走下去了,既然這條路走到底沒找到關鍵信息,那麼就折回頭看看其餘方法。這裏調用了 AnimationHandler 類的 addAnimationFrameCallback()
,新出現了一個類,看命名應該是專門處理動畫相關的,並且仍是單例類,跟進去看看:
首先第二個參數 delay 取決於咱們是否調用了
setStartDelay()
來設置動畫的延遲執行,假設目前的動畫都沒有設置,那麼 delay 也就是 0,因此這裏着重看一下前面的代碼。
mAnimationCallbacks 是一個 ArrayList,每一項保存的是 AnimationFrameCallback 接口的對象,看命名這是一個回調接口,那麼是誰在何時會對它進行回調呢?根據目前僅有的信息,咱們並無辦法看出來,那麼能夠先放着,這裏只要記住第一個參數以前傳進來的是 this,也就是說若是這個接口被回調時,那麼 ValueAnimator 對這個接口的實現將會被回調。
接下去開始按順序過代碼了,當 mAnimationCallbacks 列表大小等於 0 時,將會調用一個方法,很明顯,若是動畫是第一次執行的話,那麼這個列表大小應該就是 0,由於將 callback 對象添加到列表裏的操做是在這個判斷以後,因此這裏咱們能夠跟進看看:
哇,這麼快就看到 Choreographer 了,感受咱們好像已經快接近真相了,繼續跟下去:
因此內部實際上是調用了 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 又繼續作了什麼:
其實就作了兩件事,一件是去處理動畫的相關工做,也就是說要找到動畫真正執行的地方,跟着 doAnimationFrame()
往下走應該就好了。而剩下的代碼就是處理另一件事:繼續向底層註冊監聽下一個屏幕刷新信號。
先講講第二件事,咱們知道,動畫是一個持續的過程,也就是說,每一幀都應該處理一個動畫進度,直到動畫結束。既然這樣,咱們就須要在動畫結束以前的每個屏幕刷新信號都可以接收到,因此在每一幀裏都須要再去向底層註冊監聽下一個屏幕刷新信號事件。因此你會發現,上面代碼裏參數是 this,也就是 mFrameCallback 自己,結合一下以前的那個流程,這裏能夠獲得的信息是:
當第一個屬性動畫調用了 start()
時,因爲 mAnimationCallbacks 列表此時大小爲 0,因此直接由 addAnimationFrameCallback()
方法內部間接的向底層註冊下一個屏幕刷新信號事件,而後將該動畫加入到列表裏。而當接收到屏幕刷新信號時,mFrameCallback 的 doFrame()
會被回調,該方法內部作了兩件事,一是去處理當前幀的動畫,二則是根據列表的大小是否不爲 0 來決定繼續向底層註冊監聽下一個屏幕刷新信號事件,如此反覆,直至列表大小爲 0。
因此,這裏能夠猜想一點,若是當前動畫結束了,那麼就須要將其從 mAnimationCallbacks 列表中移除,這點能夠後面跟源碼過程當中來驗證。
那麼,下去就是跟着 doAnimationFrame()
來看看,屬性動畫是怎麼執行的:
這裏歸納下其實就作了兩件事:
一是去循環遍歷列表,取出每個 ValueAnimator,而後判斷動畫是否有設置了延遲開始,或者說動畫是否到時間該執行了,若是到時間執行了,那麼就會去調用 ValueAnimator 的 doAnimationFrame()
;
二是調用了 cleanUpList()
方法,看命名就能夠猜想是去清理列表,那麼應該也就是處理掉已經結束的動畫,由於 AnimationHandler 是爲全部屬性動畫服務的,同一時刻也許有多個動畫正在進行中,那麼動畫的結束確定有前後,已經結束的動畫確定要從列表中移除,這樣等全部動畫都結束了,列表大小變成 0 了,mFrameCallback 才能夠中止向底層註冊監聽下一個屏幕刷新信號事件,AnimationHandler 才能夠進入空閒狀態,不用再每一幀都去處理動畫的工做。
那麼,咱們優先看看 cleanUpList()
,由於感受它的工做比較簡單,那就先梳理掉:
猜想正確,將列表中爲 null 的對象都移除掉,那麼咱們就能夠繼續進一步猜想,動畫若是結束的話,會將自身在這個列表中的引用賦值爲 null,這點能夠在稍微跟蹤動畫的流程中來進行確認。
清理的工做梳理完,那麼接下去就是繼續去跟着動畫的流程了,還記得咱們上面提到了另外一件事是遍歷列表去調用每一個動畫 ValueAnimator 的 doAnimationFrame()
來處理動畫邏輯麼,那麼咱們接下去就跟進這個方法看看:
上面省略了部分代碼,省略的那些代碼跟動畫是否被暫停或從新開始有關,本篇優先梳理正常的動畫流程,這些就先不關注了。
稍微歸納一下,這個方法內部其實就作了三件事:
一是處理第一幀動畫的一些工做;
二是根據當前時間計算當前幀的動畫進度,因此動畫的核心應該就是在 animateBaseOnTime()
這個方法裏,意義就相似 Animation 動畫的 getTransformation()
方法;
三是判斷動畫是否已經結束了,結束了就去調用 endAnimation()
,按照咱們以前的猜想,這個方法內應該就是將當前動畫從 mAniamtionCallbacks 列表裏移除。
咱們先來看動畫結束以後的處理工做,由於上面纔剛梳理了一部分,趁着如今大夥還有些印象,並且這部分工做會簡單易懂點,先把簡單的吃掉:
很簡單,兩件事,一是去通知說動畫結束了,二是調用了 AniamtionHandler 的 removeCallback()
,繼續跟進看看:
咱們以前的猜想在這裏獲得驗證了吧,若是動畫結束,那麼它會將其自身在 AnimationCallbacks 列表裏的引用賦值爲 null,而後移出列表的工做就交由 AnimationHandler 去作。咱們說了,AnimationHandler 是爲全部的屬性動畫服務的,那麼當某個動畫結束的話,就必須進行一些資源清理的工做,整個清理的流程邏輯就是咱們目前梳理出來的這樣。
好,搞定了一個小點了,那麼接下去繼續看剩下的兩件事,先看第一件,處理動畫第一幀的工做問題:
參考 Animation 動畫的原理,第一幀的工做一般都是爲了記錄動畫第一幀的時間戳,由於後續的每一幀裏都須要根據當前時間以及動畫第一幀的時間還有一個動畫持續時長來計算當前幀動畫所處的進度,Animation 動畫咱們梳理過了,因此這裏在過第一幀的邏輯時應該就會比較有條理點。咱們來看看,屬性動畫的第一幀的工做是否是跟 Animation 差很少:
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 了。
就像咱們看電視時,咱們不想看片頭,因此直接選擇從正片開始看,相似的道理。
好了,還記得前面說了處理動畫第一幀的工做大致上有兩件事,另外一件是調用了一個方法麼。咱們回頭來看看,這裏又是作了些什麼:
只是將 ValueAnimator 添加到 AnimationHandler 裏的另外一個列表中去,能夠過濾這個列表的變量名看看它都在哪些地方被使用到了:
這地方還記得吧,咱們上面分析的那一大堆工做都是跟着
callback.doAnimationFrame(frameTime)
這行代碼走進去的,雖然內部作的事咱們還沒所有分析完,但咱們這裏能夠知道,等內部全部事都完成後,會退回到 AnimationHandler 的 doAnimationFrame()
繼續往下幹活,因此再繼續跟下去看看:
上面說過,Choreographer 內部有多個隊列,每一個隊列裏均可以存放 FrameCallback 對象,或者 Runnable 對象。此次是傳到了另外一個隊列裏,傳進的是一個 Runnable 對象,咱們看看這個 Runnable 作了些什麼:
ValueAnimator 實現了 AnimationFrameCallback 接口,這裏等因而回調了 ValueAnimator 的方法,而後將其從隊列中移除。看看 ValueAnimator 的實現作了些什麼:
好嘛,這裏說穿了其實也是在修正動畫的第一幀時間 mStartTime。那麼,其實也就是說,ValueAnimator 的 doAnimationFrame()
裏處理第一幀工做的兩件事所有都是用於計算動畫的第一幀時間,只是一件是根據是否 "跳過片頭"( setCurrentPlayTime()
) 計算,另外一件則是這裏的修正。
那麼,這裏爲何要對第一幀時間 mStartTime 進行修正呢?
大夥有時間能夠去看看 AnimationFrameCallback 接口的 commitAnimationFrame()
方法註釋,官方解釋得特別清楚了,我這裏就不貼圖了,直接將個人理解寫出來:
其實,這跟屬性動畫經過 Choreographer 的實現原理有關。咱們知道,屏幕的刷新信號事件都是由 Choreographer 負責,它內部有多個隊列,這些隊列裏存放的工做都是用於在接收到信號時取出來處理。那麼,這些隊列有什麼區別呢?
其實也就是執行的前後順序的區別,按照執行的前後順序,咱們假設這些隊列的命名爲:1隊列 > 2隊列 > 3隊列。咱們本篇分析的屬性動畫,AnimationHandler 封裝的 mFrameCallback 工做就是放到 1隊列裏的;而以前分析的 Animation 動畫,它經過 ViewRootImpl 封裝的 doTraversal()
工做是放到 2隊列裏的;而上面剛過完的修正動畫第一幀時間的 Runnable 工做則是放到 3隊列裏的。
也就是說,當接收到屏幕刷新信號後,屬性動畫會最早被處理。而後是去計算當前屏幕數據,也就是測量、佈局、繪製三大流程。可是這樣會有一個問題,若是頁面太過複雜,繪製當前界面時花費了太多的時間,那麼等到下一個屏幕刷新信號時,屬性動畫根據以前記錄的第一幀時間戳計算動畫進度時,會發現丟了開頭的好幾幀,明明動畫沒還執行過。因此,這就是爲何須要對動畫第一幀時間進行修正。
固然,若是動畫已經開始了,在動畫中間某一幀,就不會去修正了,這個修正,只是針對動畫的第一幀時間。由於,若是是在第一幀發現繪製界面太耗時,丟了開頭幾幀,那麼咱們能夠經過延後動畫開始的時機來達到避免丟幀。但若是是在動畫執行過程當中才遇到繪製界面太耗時,那無論什麼策略都沒法避免丟幀了。
好了,到這裏,大夥先休息下,咱們來梳理一下目前全部的信息,否則我估計大夥已經忘了上面講過什麼了:
ValueAnimator 屬性動畫調用了 start()
以後,會先去進行一些初始化工做,包括變量的初始化、通知動畫開始事件;
而後經過 AnimationHandler 將其自身 this 添加到 mAnimationCallbacks 隊列裏,AnimationHandller 是一個單例類,爲全部的屬性動畫服務,列表裏存放着全部正在進行或準備開始的屬性動畫;
若是當前存在要運行的動畫,那麼 AnimationHandler 會去經過 Choreographer 向底層註冊監聽下一個屏幕刷新信號,當接收到信號時,它的 mFrameCallback 會開始進行工做,工做的內容包括遍歷列表來分別處理每一個屬性動畫在當前幀的行爲,處理完列表中的全部動畫後,若是列表還不爲 0,那麼它又會經過 Choreographer 再去向底層註冊監聽下一個屏幕刷新信號事件,如此反覆,直至全部的動畫都結束。
AnimationHandler 遍歷列表處理動畫是在 doAnimationFrame()
中進行,而具體每一個動畫的處理邏輯則是在各自,也就是 ValueAnimator 的 doAnimationFrame()
中進行,各個動畫若是處理完自身的工做後發現動畫已經結束了,那麼會將其在列表中的引用賦值爲空,AnimationHandler 最後會去將列表中全部爲 null 的都移除掉,來清理資源。
每一個動畫 ValueAnimator 在處理自身的動畫行爲時,首先,若是當前是動畫的第一幀,那麼會根據是否有"跳過片頭"(setCurrentPlayTime()
)來記錄當前動畫第一幀的時間 mStartTime 應該是什麼。
第一幀的動畫其實也就是記錄 mStartTime 的時間以及一些變量的初始化而已,動畫進度仍然是 0,因此下一幀纔是動畫開始的關鍵,但因爲屬性動畫的處理工做是在繪製界面以前的,那麼有可能由於繪製耗時,而致使 mStartTime 記錄的第一幀時間與第二幀之間隔得過久,形成丟了開頭的多幀,因此若是是這種狀況下,會進行 mStartTime 的修正。
修正的具體作法則是當繪製工做完成後,此時,再根據當前時間與 mStartTime 記錄的時間作比較,而後進行修正。
若是是在動畫過程當中的某一幀纔出現繪製耗時現象,那麼,只能表示無能爲力了,丟幀是避免不了的了,想要解決就得本身去分析下爲何繪製會耗時;而若是是在第一幀是出現繪製耗時,那麼,系統仍是能夠幫忙補救一下,修正下 mStartTime 來達到避免丟幀。
好了,休息結束,咱們繼續,還有一段路要走,其實整個流程目前大致上已經出來了,只是缺乏了當前幀的動畫進度具體計算實現細節,這部分估計會更讓人頭大。
以前分析 ValueAnimator 的 doAnimationFrame()
時,咱們將其歸納出來主要作了三件事:一是處理第一幀動畫的工做;二是根據當前時間計算並實現當年幀的動畫工做;三是根據動畫是否結束進行一些資源清理工做;一三咱們都分析了,下面就來過過第二件事,animateBasedOnTime()
:
從這裏開始,就是在計算當前幀的動畫邏輯了,整個過程跟 Animation 動畫基本上差很少。上面的代碼裏,我省略了一部分,那部分是用於根據是否設置的 mRepeatCount 來處理動畫結束後是否須要從新開始,這些咱們就不看了,咱們着重梳理一個正常的流程下來便可。
因此,歸納一下,這個方法裏其實也就是作了三件事:
一是,根據當前時間以及動畫第一幀時間還有動畫持續的時長來計算當前的動畫進度。
二是,確保這個動畫進度的取值在 0-1 之間,這裏調用了兩個方法來輔助計算,咱們就不跟進去了,之因此有這麼多的輔助計算,那是由於,屬性動畫支持 setRepeatCount()
來設置動畫的循環次數,而從始至終的動畫第一幀的時間都是 mStrtTime 一個值,因此在第一個步驟中根據當前時間計算動畫進度時會發現進度值是可能會超過 1 的,好比 1.5, 2.5, 3.5 等等,因此第二個步驟的輔助計算,就是將這些值等價換算到 0-1 之間。
三就是最重要的了,當前幀的動畫進度計算完畢以後,就是須要應用到動畫效果上面了,因此 animateValue()
方法的意義就是相似於 Animation 動畫中的 applyTransformation()
。
咱們都說,屬性動畫是經過修改屬性值來達到動畫效果的,那麼咱們就跟着 animateValue()
進去看看:
這裏乾的活我也大概的給劃分紅了三件事:
一是,根據插值器來計算當前的真正的動畫進度,插值器算是動畫裏比較重要的一個概念了,可能平時用的少,若是咱們沒有明確指定使用哪一個插值器,那麼系統一般會有一個默認的插值器。
二是,根據插值器計算獲得的實際動畫進度值,來映射到咱們須要的數值。這麼說吧,就算通過了插值器計算以後,動畫進度值也只是 0-1 區間內的某個值而已。而咱們一般須要的並非 0-1 的數值,好比咱們但願一個 0-500 的變化,那麼咱們就須要本身在拿到 0-1 區間的進度值後來進行轉換。第二個步驟,大致上的工做就是幫助咱們處理這個工做,咱們只須要告訴 ValueAnimator 咱們須要 0-500 的變化,那麼它在拿到進度值後會進行轉換。
三就只是通知動畫的進度回調而已了。
流程上差很少已經梳理出來了,不過我我的對於內部是如何根據拿到的 0-1 區間的進度值轉換成咱們指定區間的數值的工做挺感興趣的,那麼咱們就稍微再深刻去分析一下好了。這部分工做主要就是調用了 mValues[i].calculateValue(fraction)
這一行代碼來實現,mValues 是一個 PropertyValuesHolder 類型的數組,因此關鍵就是去看看這個類的 calculateValue()
作了啥:
咱們在使用 ValueAnimator 時,註冊了動畫進度回調,而後在回調裏取當前的值時其實也就是取到上面那個 mAnimatedValue 變量的值,而這個變量的值是經過 mKeyframes.getValue()
計算出來的,那麼再繼續跟進看看:
KeyFrames 是一個接口,那麼接下去就是要找找哪裏實現了這個接口:
具體的找法,能夠在 PropertyValuesHolder 這個類裏利用 Ctrl + F
過濾一下 mKeyframes =
來看一下它在哪些地方被實例化了。匹配到的地方不少,但都差很少,都是經過 KeyframeSet 的 ofXXX 方法實例化獲得的對象,那麼具體的實現應該就是在 KeyframeSet 這個類裏了。
在跟進去看以前,有一點想提一下,大夥應該注意到了吧,mKeyframes 實例化的這些地方,ofInt()
,onFloat()
等等是否是很熟悉。沒錯,就是咱們建立屬性動畫時類似的方法名, 其實 ValueAnimator.ofInt()
內部會根據相應的方法來建立 mKeyframes 對象,也就是說,在實例化屬性動畫時,這些 mKeyframes 也順便被實例化了。想確認的,大夥能夠本身去跟下源碼看看,我這裏就不貼了。
好了,接下去看看 KeyframeSet 這個類的 ofInt()
方法,看看它內部具體是建立了什麼:
這裏又涉及到新的機制了吧,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()
方法裏看看它是怎麼使用這些關鍵幀的:
因此關鍵的工做就都在 getIntValue()
這裏了,參數傳進來還記得是什麼吧,就是通過插值器計算以後當前幀的動畫進度值,0-1 區間的那個值,getIntValue()
這個方法的代碼有些多,咱們一塊一塊來看,先看第一塊:
當關鍵幀只有兩幀時,咱們常使用的 ValueAnimator.ofInt(100)
, 內部其實就是隻建立了兩個關鍵幀,一個是起點 0,一個是結束點 100。那麼,在這種只有兩幀的狀況下,將 0-1 的動畫進度值轉換成咱們須要的 0-100 區間內的值,系統的處理很簡單,若是沒有設置估值器,也就是 mEvaluator,那麼就直接是按比例來轉換,好比進度爲 0.5,那按比例轉換就是 (100 - 0) * 0.5 = 50。若是有設置估值器,那就按照估值器定的規則來,估值器其實就是相似於插值器,屬性動畫裏才引入的概念,Animation 動畫並無,由於只有屬性動畫內部才幫咱們作了值轉換工做。
上面是當關鍵幀只有兩幀時的處理邏輯,那麼當關鍵幀超過兩幀的時候呢:
當關鍵幀超過兩幀時,分三種狀況來處理:第一幀的處理;最後一幀的處理;中間幀的處理;
那麼,何時關鍵幀會超過兩幀呢?其實也就是咱們這麼使用的時候:ValueAnimator.ofInt(0, 100, 0, -100, 0)
,相似這種用法的時候關鍵幀就不止兩個了,這時候數量就是根據參數的個數來決定的了。
那麼,咱們再來詳細看看三種狀況的處理邏輯,首先是第一幀的處理邏輯:
fraction <= 0f 表示的應該不止是第一幀的意思,但除了理解成第一幀外,我不清楚其餘場景是什麼,暫時以第一幀來理解,這個應該影響不大。
處理的邏輯其實也很簡單,還記得當只有兩個關鍵幀時是怎麼處理的吧。那在處理第一幀的工做時,只須要將第二幀當成是最後一幀,那麼第一幀和第二幀這樣也就能夠當作是隻有兩幀的場景了吧。可是參數 fraction 動畫進度是以實際第一幀到最後一幀計算出來的,因此須要先對它進行轉換,換算出它在第一幀到第二幀之間的進度,接下去的邏輯也就跟處理兩幀時的邏輯是同樣的了。
一樣的道理,在處理最後一幀時,只須要取出倒數第一幀跟倒數第二幀的信息,而後將進度換算到這兩針之間的進度,接下去的處理邏輯也就是同樣的了。代碼我就不貼了。
但處理中間幀的邏輯就不同了,由於根據 0-1 的動畫進度,咱們能夠很容易區分是處於第一幀仍是最後一幀,無非一個就是 0,一個是 1。可是,當動畫進度值在 0-1 之間時,咱們並無辦法直接看出這個進度值是落在中間的哪兩個關鍵幀之間,若是有辦法計算出當前的動畫進度處於哪兩個關鍵幀之間,那麼接下去的邏輯也就是同樣的了,因此關鍵就是在於找出當前進度處於哪兩個關鍵幀之間:
系統的找法也很簡單,從第一幀開始,按順序遍歷每一幀,而後去判斷當前的動畫進度跟這一幀保存的位置信息來找出當前進度是否就是落在某兩個關鍵幀之間。由於每一個關鍵幀保存的信息除了有它對應的值以外,還有一個是它在第一幀到最後一幀之間的哪一個位置,至於這個位置的取值是什麼,這就是由在建立這一系列關鍵幀時來控制的了。
還記得是在哪裏建立了這一系列的關鍵幀的吧,回去 KeyframeSet 的 ofInt()
裏看看:
在建立每一個關鍵幀時,傳入了兩個參數,第一個參數就是表示這個關鍵幀在整個區域之間的位置,第二參數就是它表示的值是多少。看上面的代碼, i 表示的是第幾幀,numKeyframes 表示的是關鍵幀的總數量,因此 i/(numKeyframes - 1) 也就是表示這一系列關鍵幀是按等比例來分配的。
好比說, ValueAnimator.ofInt(0, 50, 100, 200)
,這總共有四個關鍵幀,那麼按等比例分配,第一幀就是在起點位置 0,第二幀在 1/3 位置,第三幀在 2/3 的位置,最後一幀就是在 1 的位置。
到這裏,咱們再來梳理一下後面部分過的內容:
當接收到屏幕刷新信號後,AnimationHandler 會去遍歷列表,將全部待執行的屬性動畫都取出來去計算當前幀的動畫行爲。
每一個動畫在處理當前幀的動畫邏輯時,首先會先根據當前時間和動畫第一幀時間以及動畫的持續時長來初步計算出當前幀時動畫所處的進度,而後會將這個進度值等價轉換到 0-1 區間以內。
接着,插值器會將這個通過初步計算以後的進度值根據設定的規則計算出實際的動畫進度值,取值也是在 0-1 區間內。
計算出當前幀動畫的實際進度以後,會將這個進度值交給關鍵幀機制,來換算出咱們須要的值,好比 ValueAnimator.ofInt(0, 100) 表示咱們須要的值變化範圍是從 0-100,那麼插值器計算出的進度值是 0-1 之間的,接下去就須要藉助關鍵幀機制來映射到 0-100 之間。
關鍵幀的數量是由 ValueAnimator.ofInt(0, 1, 2, 3) 參數的數量來決定的,好比這個就有四個關鍵幀,第一幀和最後一幀是必須的,因此最少會有兩個關鍵幀,若是參數只有一個,那麼第一幀默認爲 0,最後一幀就是參數的值。當調用了這個 ofInt() 方法時,關鍵幀組也就被建立了。
當只有兩個關鍵幀時,映射的規則是,若是沒有設置估值器,那麼就等比例映射,好比動畫進度爲 0.5,須要的值變化區間是 0-100,那麼等比例映射後的值就是 50,那麼咱們在 onAnimationUpdate 的回調中經過 animation.getAnimatedValue() 獲取到的值 50 就是這麼來的。
若是有設置估值器,那麼就按估值器的規則來進行映射。
當關鍵幀超過兩個時,須要先找到當前動畫進度是落於哪兩個關鍵幀之間,而後將這個進度值先映射到這兩個關鍵幀之間的取值,接着就能夠將這兩個關鍵幀當作是第一幀和最後一幀,那麼就能夠按照只有兩個關鍵幀的狀況下的映射規則來進行計算了。
而進度值映射到兩個關鍵幀之間的取值,這就須要知道每一個關鍵幀在整個關鍵幀組中的位置信息,或者說權重。而這個位置信息是在建立每一個關鍵幀時就傳進來的。onInt() 的規則是全部關鍵幀按等比例來分配權重,好比有三個關鍵幀,第一幀是 0,那麼第二幀就是 0.5, 最後一幀 1。
至此,咱們已經將整個流程梳理出來了,兩部分小結的內容整合起來就是此次梳理出來的整個屬性動畫從 start()
以後,到咱們在 onAnimationUpdate 回調中取到咱們須要的值,再到動畫結束後如何清理資源的整個過程當中的原理解析。
梳理清楚後,大夥應該就要清楚,屬性動畫是如何接收到屏幕刷新信號事件的?是如何反覆接收到屏幕刷新信號事件直到整個動畫執行結束?方式是不是有區別於 Animation 動畫的?計算當前幀的動畫工做都包括了哪些?是如何將 0-1 的動畫進度映射到咱們須要的值上面的?
若是看完本篇,這些問題你內心都有譜了,那麼就說明,本篇的主要內容你都吸取進去了。固然,若是有錯的地方,歡迎指出來,畢竟內容確實不少,頗有可能存在寫錯的地方沒發現。
來張時序圖結尾:
最後,有一點想提的是,咱們本篇只是過完了 ValueAnimator 的整個流程原理,但這整個過程當中,注意到了沒有,咱們並無看到有任何一個地方涉及到了 ui 操做。在上一篇博客 Android 屏幕刷新機制中,咱們也清楚了,界面的繪製其實就是交由 ViewRootImpl 來發起的,但很顯然,ValueAnimator 跟 ViewRootImpl 並無任何交集。
那麼,ValueAnimator 又是怎麼實現動畫效果的呢?其實,ValueAnimator 只是按照咱們設定的變化區間(ofInt(0, 100)
),持續時長(setDuration(1000)
),插值器規則,估值器規則,內部在每一幀內經過一系列計算,轉換等工做,最後輸出每一幀一個數值而已。而若是要實現一個動畫效果,那麼咱們只能在進度回調接口取到這個輸出的值,而後手動應用到某個 View 上面(mView.setX()
)。因此,這種使用方式,本質上仍然是經過 View 的內部方法最終走到 ViewRootImpl 去觸發界面的更新繪製。
而 ObjectAnimator 卻又不一樣了,它內部就有涉及到 ui 的操做,具體原理是什麼,留待後續再分析。
都說屬性動畫是經過改變屬性值來達到動畫效果的,計劃寫這一篇時,原本覺得能夠梳理清楚這點的,誰知道單單只是把 ValueAnimator 的流程原理梳理出來篇幅就這麼長了,因此 ObjectAnimator 就另找時間再來梳理吧,這個問題就做爲遺留問題了。
Q1:都說屬性動畫是經過改變屬性值來達到動畫效果的,那麼它的原理是什麼呢?
最近(2018.03)剛開通了公衆號,想激勵本身堅持寫做下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的能夠點一波關注,謝謝支持~~