本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android
此次想來梳理一下 View 動畫也就是補間動畫(ScaleAnimation, AlphaAnimation, TranslationAnimation...)這些動畫運行的流程解析。內容並不會去分析動畫的呈現原理是什麼,諸如 Matrix 這類的原理是什麼,由於我也還沒搞懂。本篇主要是分析當調用了 View.startAnimation()
以後,動畫從開始到結束的一個運行流程是什麼?微信
看源碼最好是帶着問題去,這樣比較有目的性和針對性,能夠防止閱讀源碼時走偏和鑽牛角,因此咱們就先來提幾個問題。app
Animation 動畫的擴展性很高,系統只是簡單的爲咱們封裝了幾個基本的動畫:平移、旋轉、透明度、縮放等等,感興趣的能夠去看看這幾個動畫的源碼,它們都是繼承自 Animation 類,而後實現了 applyTransformation() 方法,在這個方法裏經過 Transformation 和 Matrix 實現各類各樣炫酷的動畫,因此,若是想要作出炫酷的動畫效果,這些仍是須要去搞懂的。源碼分析
目前我也還沒搞懂,能力有限,因此優先分析動畫的一個運行流程。佈局
首先看看 Animation 動畫的基本用法:
動畫
咱們要使用一個 View 動畫時,通常都是先 new 一個動畫,而後配置各類參數,最後調用動畫要做用到的那個 View 的 startAnimation(), 將動畫實例做爲參數傳進去,接下去就能夠看到動畫運行的效果了。this
那麼,問題來了:spa
Q1:不知道大夥想過沒有,當調用了 View.startAnimation() 以後,動畫是立刻就執行了麼?日誌
Q2:假如動畫持續時間 300ms,當調用了 View.startAniamtion() 以後,又發起了一次界面刷新的操做,那麼界面的刷新是在 300ms 以後也就是動畫執行完畢以後才執行的,仍是在動畫執行過程當中界面刷新操做就執行了呢?code
咱們都知道,applyTransformation() 這個方法是動畫生效的地方,這個方法被回調時參數會傳進來當前動畫的進度(0.0 ——— 1.0)。就像數學上的畫曲線,當給的點越多時畫的曲線越光滑,一樣當這個方法被回調越屢次時,動畫的效果越流暢。
好比一個從 0 放大到 1280 的 View 放大動畫,若是這過程該方法只回調 3 次的話,那麼每次的跨度就會很大,好比 0 —— 600 —— 1280,那麼這個動畫效果看起來就會很突兀;相反,若是這過程該方法回調了幾十次的話,那麼每次跨度可能就只有 100,這樣一來動畫效果看起來就會很流暢。
相信大夥也都有過在 applyTransformation() 裏打日誌來查看當前的動畫進度,有時打出的日誌有十幾條,有時卻又有幾十條。
那麼咱們的問題就來了:
Q3:applyTransformation() 這個方法的回調次數是根據什麼來決定的?
好了,本篇就是主要講解這三個問題,這三個問題搞明白的話,之後碰到動畫卡頓的時候就懂得如何去分析、定位丟幀的地方了,找到丟幀的問題所在後離解決問題也就不遠了。
ps:本篇分析的源碼全都基於 android-25 版本。如下源碼均採用截圖方式,每張圖最上面是類名+方法名,大夥想本身過一遍的時候,若是不清楚方法屬於哪一個類的能夠在每張圖最上面查看。
剛開始接觸源碼分析可能不清楚該從哪入手,建議能夠從咱們使用它的地方來 startAnimation()
:
代碼很少,調用了四個方法,那麼一個個跟進去看看,先是 setStartTime()
:
因此這裏只是對一些變量進行賦值,並無運行動畫的邏輯,繼續看看 setAnimation()
:
View 裏面有一個 Animation 類型的成員變量,因此這個方法實際上是將咱們 new 的 ScaleAnimation 動畫跟 View 綁定起來而已,也沒有運行動畫的邏輯,繼續往下看看 invalidateParentCached()
:
invalidateParentCaches()
這方法更簡單,給 mPrivateFlags 添加了一個標誌位,雖然還不清楚幹嗎的,但能夠先留個心眼,由於 mPrivateFlags 這個變量在閱讀跟 View 相關的源碼時常常碰到,那麼能夠的話能搞明白就搞明白,但目前跟咱們想要找出動畫到底何時開始執行的關係好像不大,先略過,繼續跟進 invalidate()
:
因此 invalidate()
內部實際上是調用了 ViewGroup 的 invalidateChild()
,再跟進看看:
這裏有一個 do{}while() 的循環操做,第一次循環的時候 parent 是 this,即 ViewGroup 自己,因此接下去就是調用 ViewGroup 自己的 invalidateChildInParent()
方法,而後循環終止條件是 patent == null,因此能夠猜想這個方法返回的應該是 ViewGroup 的 parent,跟進看看:
因此關鍵是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 這兩個是何時賦值給 mPrivateFlags,由於只要有兩個標誌中的一個時,該方法就會返回 mParent,具體賦值的地方還不大清楚,但能肯定的是動畫執行時,它是知足 if 條件的,也就是這個方法會返回 mParent。
一個具體的 View 的 mParent 是 ViewGroup,ViewGroup 的 mParent 也是 ViewGoup,因此在 do{}while() 循環裏會一直不斷的尋找 mParent,而一顆 View 樹最頂端的 mParent 是 ViewRootImpl,因此最終是會走到了 ViewRootImpl 的 invalidateChildInParent()
裏去了。
至於一個界面的 View 樹最頂端爲何是 ViewRootImpl,這個就跟 Activity 啓動過程有關了。咱們都清楚,在 onCreate 裏 setContentView() 的時候,是將咱們本身寫的佈局文件添加到以 DecorView 爲根佈局的一個 ViewGroup 裏,也就是說 DevorView 纔是 View 樹的根佈局,那爲何又說 View 樹最頂端實際上是 ViewRootImpl 呢?
這是由於在 onResume()
執行完後,WindowManager 將會執行 addView()
,而後在這裏面會去建立一個 ViewRootImpl 對象,接着將 DecorView 跟 ViewRootImpl 對象綁定起來,而且將 DecorView 的 mParent 設置成 ViewRootImpl,而 ViewRootImpl 是實現了 ViewParent 接口的,因此雖然 ViewRootImpl 沒有繼承 View 或 ViewGroup,但它確實是 DecorView 的 parent。這部份內容應該屬於 Activity 的啓動過程相關原理的,因此本篇只給出結論,不深刻分析了,感興趣的能夠自行搜索一下。
那麼咱們繼續返回到尋找動畫執行的地方,咱們跟到了 ViewRootImpl 的 invalidateChildInParent()
裏去了,看看它作了些什麼:
首先第一點,它的全部返回值都是 null,因此以前那個 do{}while() 循環最終就是執行到這裏後確定就會中止了。而後參數 dirty 是在最初 View 的 invalidateInternal()
裏層層傳遞過來的,能夠確定的是它不爲空,也不是 isEmpty,因此繼續跟到 invalidateRectOnScreen()
方法裏看看:
跟到這裏就能夠了,scheduleTraversals()
做用是將 performTraversals()
封裝到一個 Runnable 裏面,而後扔到 Choreographer 的待執行隊列裏,這些待執行的 Runnable 將會在最近的一個 16.6 ms 屏幕刷新信號到來的時候被執行。而 performTraversals()
是 View 的三大操做:測量、佈局、繪製的發起者。
View 樹裏面無論哪一個 View 發起了佈局請求、繪製請求,通通最終都會走到 ViewRootImpl 裏的 scheduleTraversals(),而後在最近的一個屏幕刷新信號到了的時候再經過 ViewRootImpl 的 performTraversals() 從根佈局 DecorView 開始依次遍歷 View 樹去執行測量、佈局、繪製三大操做。這也是爲何一直要求頁面佈局層次不能太深,由於每一次的頁面刷新都會先走到 ViewRootImpl 裏,而後再層層遍歷到具體發生改變的 View 裏去執行相應的佈局或繪製操做。
這些內容應該是屬於 Android 屏幕刷新機制的,這裏就先只給出結論,具體分析我會在幾天後再發一篇博客出來。
因此,咱們從 View.startAnimation()
開始跟進源碼分析的這一過程當中,也能夠看出,執行動畫,其實內部會調用 View 的重繪請求操做 invalidate()
,因此最終會走到 ViewRootImpl 的 scheduleTraversals()
,而後在下一個屏幕刷新信號到的時候去遍歷 View 樹刷新屏幕。
因此,到這裏能夠獲得的結論是:
當調用了 View.startAniamtion() 以後,動畫並無立刻就被執行,這個方法只是作了一些變量初始化操做,接着將 View 和 Animation 綁定起來,而後調用重繪請求操做,內部層層尋找 mParent,最終走到 ViewRootImpl 的 scheduleTraversals 裏發起一個遍歷 View 樹的請求,這個請求會在最近的一個屏幕刷新信號到來的時候被執行,調用 performTraversals 從根佈局 DecorView 開始遍歷 View 樹。
那麼,到這裏,咱們能夠猜想,動畫其實真正執行的地方應該是在 ViewRootImpl 發起的遍歷 View 樹的這個過程當中。測量、佈局、繪製,View 顯示到屏幕上的三個基本操做都是由 ViewRootImpl 的 performTraversals()
來控制,而做爲 View 樹最頂端的 parent,要控制這顆 Veiw 樹的三個基本操做,只能經過層層遍歷。因此,測量、佈局、繪製三個基本操做的執行都會是一次遍歷操做。
我在跟着這三個流程走的時候,最後發現,在跟着繪製流程走的時候,看到了跟動畫相關的代碼,因此咱們就跳過其餘兩個流程,直接看繪製流程:
這張圖不是我畫的,在網上找的,繪製流程的開始是由 ViewRootImpl 發起的,而後從 DecorView 開始遍歷 View 樹。而遍歷的實現,是在 View#draw() 方法裏的。咱們能夠看看這個方法的註釋:
這個方法裏主要作了上述六件事,大致上就是若是當前 View 須要繪製,就會去調用本身的 onDraw()
,而後若是有子 View,就會調用dispatchDraw()
將繪製事件通知給子 View。ViewGroup 重寫了 dispatchDraw()
,調用了 drawChild()
,而 drawChild()
調用了子 View 的 draw(Canvas, ViewGroup, long)
,而這個方法又會去調用到 draw(Canvas)
方法,因此這樣就達到了遍歷的效果。整個流程就像上上圖中畫的那樣。
在這個流程中,當跟到 draw(Canvas, ViewGroup, long)
裏時,發現了跟動畫相關的代碼:
還記得咱們調用 View.startAnimation(Animation)
時將傳進來的 Animation 賦值給 mCurrentAnimation 了麼。
因此當時傳進來的 Animation ,如今拿出來用了,那麼動畫真正執行的地方應該也就是在 applyLegacyAnimation()
方法裏了(該方法在 android-22 版本及以前的命名是 drawAnimation)
這下肯定動畫真正開始執行是在什麼地方了吧,都看到 onAnimationStart()
了,也看到了對動畫進行初始化,以及調用了 Animation 的 getTransformation
,這個方法是動畫的核心,再跟進去看看:
這個方法裏作了幾件事:
因此,到這裏咱們已經能肯定 applyTransformation()
是何時回調的,動畫是何時才真正開始執行的。那麼 Q1 總算是搞定了,Q2 也基本能理清了。由於咱們清楚, applyTransformation()
最終是在繪製流程中的 draw()
過程當中執行到的,那麼顯然在每一幀的屏幕刷新信號來的時候,遍歷 View 樹是爲了從新計算屏幕數據,也就是所謂的 View 的刷新,而動畫只是在這個過程當中順便執行的。
接下去就是 Q3 了,咱們知道 applyTransformation()
是動畫生效的地方,這個方法不斷的被回調時,參數會傳進來動畫的進度,因此呈現效果就是動畫根據進度在運行中。
可是,咱們從頭分析下來,找到了動畫真正執行的地方,找到了 applyTransformation() 被調用的地方,但這些地方都沒有看到任何一個 for 或者 while 循環啊,也就是一次 View 樹的遍歷繪製操做,動畫也就只會執行一次而已啊?那麼它是怎麼被回調那麼屢次的?
咱們知道 applyTransformation()
是在 getTransformation()
裏被調用的,而這個方法是有一個 boolean 返回值的,咱們看看它的返回邏輯是什麼:
也就是說 getTransformation()
的返回值表明的是動畫是否完成,還記得是哪裏調用的 getTransformation()
吧,去 applyLegacyAnimation()
裏看看取到這個返回值後又作了什麼:
當動畫若是還沒執行完,就會再調用 invalidate()
方法,層層通知到 ViewRootImpl 再次發起一次遍歷請求,當下一幀屏幕刷新信號來的時候,再經過 performTraversals()
遍歷 View 樹繪製時,該 View 的 draw 收到通知被調用時,會再次去調用 applyLegacyAnimation()
方法去執行動畫相關操做,包括調用 getTransformation()
計算動畫進度,調用 applyTransformation()
應用動畫。
也就是說,動畫很流暢的狀況下,實際上是每隔 16.6ms 即每一幀到來的時候,執行一次 applyTransformation()
,直到動畫完成。因此這個 applyTransformation()
被回調屢次是這麼來的,並且這個回調次數並無辦法人爲進行設定。
這就是爲何當動畫持續時長越長時,這個方法打出的日誌越屢次的緣由。
還記得 getTransformation()
方法在計算動畫進度時是根據參數傳進來的 currentTime 的麼,而這個 currentTime 能夠理解成是發起遍歷操做這個時刻的系統時間(實際 currentTime 是在 Choreographer 的 doFrame() 裏通過校驗調整以後的一個時間,但離發起遍歷操做這個時刻的系統時間相差很小,因此不深究的話,能夠像上面那樣理解,比較容易明白)。
綜上,咱們稍微整理一下:
首先,當調用了 View.startAnimation() 時動畫並無立刻就執行,而是經過 invalidate() 層層通知到 ViewRootImpl 發起一次遍歷 View 樹的請求,而此次請求會等到接收到最近一幀到了的信號時纔去發起遍歷 View 樹繪製操做。
從 DecorView 開始遍歷,繪製流程在遍歷時會調用到 View 的 draw() 方法,當該方法被調用時,若是 View 有綁定動畫,那麼會去調用applyLegacyAnimation(),這個方法是專門用來處理動畫相關邏輯的。
在 applyLegacyAnimation() 這個方法裏,若是動畫尚未執行過初始化,先調用動畫的初始化方法 initialized(),同時調用 onAnimationStart() 通知動畫開始了,而後調用 getTransformation() 來根據當前時間計算動畫進度,緊接着調用 applyTransformation() 並傳入動畫進度來應用動畫。
getTransformation() 這個方法有返回值,若是動畫還沒結束會返回 true,動畫已經結束或者被取消了返回 false。因此 applyLegacyAnimation() 會根據 getTransformation() 的返回值來決定是否通知 ViewRootImpl 再發起一次遍歷請求,返回值是 true 表示動畫沒結束,那麼就去通知 ViewRootImpl 再次發起一次遍歷請求。而後當下一幀到來時,再從 DecorView 開始遍歷 View 樹繪製,重複上面的步驟,這樣直到動畫結束。
有一點須要注意,動畫是在每一幀的繪製流程裏被執行,因此動畫並非單獨執行的,也就是說,若是這一幀裏有一些 View 須要重繪,那麼這些工做一樣是在這一幀裏的此次遍歷 View 樹的過程當中完成的。每一幀只會發起一次 perfromTraversals() 操做。
以上,就是本篇全部的內容,將 View 動畫 Animation 的運行流程原理梳理清楚,但要搞清楚爲何動畫會出現卡頓現象的話,還須要理解 Android 屏幕的刷新機制以及消息驅動機制;這些內容將在最近幾天內整理成博客分享出來。
最後仍然遺留一些還沒有解決的問題,等待繼續探索:
Q1:大夥都清楚,View 動畫區別於屬性動畫的就是 View 動畫並不會對這個 View 的屬性值作修改,好比平移動畫,平移以後 View 仍是在原來的位置上,實際位置並不會隨動畫的執行而移動,那麼這點的原理是什麼?
Q2:既然 View 動畫不會改變 View 的屬性值,那麼若是是縮放動畫時,View 須要從新執行測量操做麼?
最近剛開通了公衆號,想激勵本身堅持寫做下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的能夠點一波關注,謝謝支持~~