1. 前言
作為Android程序員,或者是想要去模仿一些酷炫的效果,或者是為了實現視覺的變態需求,或者是壓抑不住內心的創造欲想要炫技,我們不可避免地需要做各種動畫。Android中,動畫主要分為幀動畫、插間動畫以及屬性動畫。幀動畫最為簡單,是用一系列的素材作為關鍵幀逐幀播放,常用于制作加載動畫,其工作量主要在設計部分;插間動畫與屬性動畫則更多地是需要開發通過控制各種動畫參數來實現,只有系統地理解Android中動畫運行的原理,才能創作出更出色的動畫,屬性動畫在下一篇文章中分析,本文主要分享我在探索插間動畫運行原理過程中的一些收獲,包括:Matrix如何控制動畫參數;動畫中各參數具體起什么作用;透明度動畫、縮放動畫、平移動畫以及旋轉動畫的運行邏輯;動畫在View的繪制過程中如何被應用。
2. Matrix介紹
在Android中,Matrix是一個3 x 3的矩陣:
Matrix可將一個點映射到另一個點,矩陣中包含了處理縮放、透視以及平移的區域,從而可用于控制實現平移、縮放、旋轉等動畫效果。強烈建議閱讀Android Matrix理論與應用詳解以更深入地了解Matrix實現動畫控制原理,這里僅摘錄其中的關鍵信息:
結論一:設對給定的圖像依次進行了基本變化F1、F2、F3…..、Fn,它們的變化矩陣分別為T1、T2、T3…..、Tn,圖像復合變化的矩陣T可以表示為:T = TnTn-1…T1。
結論二:Preconcats matrix相當于右乘矩陣,Postconcats matrix相當于左乘矩陣。
Matrix還給我們提供了各種友好的接口來組合生成復雜的動畫,舉個例子:假如我們想要實現一個平移(a,b)之后旋轉(c,d)的動畫,那用Matrix的實現代碼就是這樣的:
Matrix matrix = new Matrix();
matrix.setTranslate(a, b);
matrix.postScale(c, d);
3. Animation運行原理分析
(1)基本屬性介紹
使用過Animation的同學對下述基本屬性應該非常熟悉,這里為了文章完整性,特地贅述一下:
- mStartTime:動畫實際開始時間
- mStartOffset:動畫延遲時間
- mFillEnabled:mFillBefore及mFillAfter是否使能
- mFillBefore:動畫結束之后是否需要進行應用動畫
- mFillAfter:動畫開始之前是否需要進行應用動畫
- mDuration:單次動畫運行時長
- mRepeatMode:動畫重復模式(RESTART、REVERSE)
- mRepeatCount:動畫重復次數(INFINITE,直接值)
- mInterceptor:動畫插間器
- mListener:動畫開始、結束、重復回調監聽器
雖然大部分都知道上面這些屬性怎么用,但是可能還是有一些人對這些字段為什么有這樣的作用不甚明白,于是我們就來分析一下。
(2)計算動畫數據
Animation在其getTransformation
函數被調用時會計算一幀動畫數據,而上面這些屬性基本都是在計算動畫數據時發光發熱,我們先看看getTransformation
函數的運行邏輯:
- 若
startTime
為START_ON_FIRST_FRAME(值為-1)
時,將startTime
設定為curTime
- 計算當前動畫進度:
normalizedTime = (curTime - (startTime + startOffset))/duration
- 若
mFillEnabled==false
:將normalisedTime
夾逼至[0.0f, 1.0f] - 判斷是否需要計算動畫數據:
- 若
normalisedTime
在[0.0f, 1.0f],需計算動畫數據 - 若
normalisedTime
不在[0.0f, 1.0f]:-
normalisedTime<0.0f
, 僅當mFillBefore==true
時才計算動畫數據 -
normalisedTime>1.0f
, 僅當mFillAfter==true
時才計算動畫數據
-
- 若
- 若需需要計算動畫數據:
- 若當前為第一幀動畫,觸發
mListener.onAnimationStart
- 若
mFillEnabled==false
:將normalisedTime
夾逼至[0.0f, 1.0f] - 根據插間器
mInterpolator
調整動畫進度:
interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
- 若動畫反轉標志位
mCycleFlip
為true
,則
interpolatedTime = 1.0 - normalizedTime
- 調用動畫更新函數
applyTransformation(interpolatedTime, transformation)
計算出動畫數據
- 若當前為第一幀動畫,觸發
- 若夾逼之前
normalisedTime
大于1.0f, 則判斷是否需繼續執行動畫:- 已執行次數
mRepeatCount
等于需執行次數mRepeated
- 若未觸發
mListener.onAnimationEnd
,則觸發之
- 若未觸發
- 已執行次數
mRepeatCount
不等于需執行次數mRepeated
- 自增
mRepeatCount
- 重置
mStartTime
為-1 - 若
mRepeatMode
為REVERSE
,則取反mCycleFlip
- 觸發
mListener.onAnimationRepeat
- 自增
- 已執行次數
這一段是根據getTransformation
源碼分析出來的,建議有興趣的同學可以直接查看源碼。上面這段分析留了一個不小的懸念,那就是動畫更新函數是什么鬼,這個函數在Animation這個抽象類中僅僅是個鉤子函數,由其子類提供具體實現,于是自然而然地引出了我們的下一個主題:主流動畫介紹。
(3)主流動畫分析
-
AlphaAnimation:透明度動畫
- 基本屬性
- mFromAlpha:起始透明度
- mToAlpha:終止透明度
- applyTransformation函數實現
- transformation.setAlpha(mFromAlpha + ((mToAlpha - mFromAlpha) * interpolatedTime))
- 基本屬性
-
ScaleAnimation:縮放動畫
- 基本屬性
- mFromX:起始X值
- mToX:終止X值
- mFromY:起始Y值
- mToY:終止Y值
- mPivotX:縮放中心點X坐標
- mPivotY:縮放中心點Y坐標
- 屬性計算邏輯
- mFromX、mToX、mFromY、mToY計算
- Float類型scale直接值
- Faction類型相對值
- 相對于自身(%):百分比轉換為float直接值
- 相對于父親(%p):根據父親size計算出size直接值,然后計算與本身size的百分比,最后轉換為float直接值
- Dimension類型size直接值:計算與本身size的百分比,然后轉換為float直接值
- mPivotX、mPivotY計算
- ABSOLUTE類型直接值
- RELATIVE_TO_SELF類型相對值:相對值乘以自身size得到直接值
- RELATIVE_TO_PARENT類型相對值:相對值乘以父親size得到直接值
- mFromX、mToX、mFromY、mToY計算
- applyTransformation函數實現
- sx = mFromX + ((mToX - mFromX) * interpolatedTime)
- sy = mFromY + ((mToY - mFromY) * interpolatedTime)
- 是否設定縮放中心點:
- 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
- 否則:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
- 基本屬性
-
TranslateAnimation:平移動畫
- 基本屬性
- mFromXDelta
- mToXDelta
- mFromYDelta
- mToYDelta
- 屬性計算邏輯
- 同ScaleAnimation中mPivotX、mPivotY的計算邏輯
- applyTransformation函數實現
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime)
- dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime)
- transformation.getMatrix().setTranslate(dx, dy)
- 基本屬性
-
RotateAnimation:旋轉動畫
- 基本屬性
- mFromDegrees
- mToDegrees
- mPivotX
- mPivotY
- 屬性計算邏輯
- mFromDegrees、mToDegrees均為角度(°)絕對值
- mPivotX、mPivotY計算邏輯同ScaleAnimation
- applyTransformation函數實現
- 是否設定縮放中心點:
- 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
- 否則:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
- 是否設定縮放中心點:
- 基本屬性
透明度、縮放、平移以及旋轉是最基本的動畫,通過組合這些動畫可以實現各種不一樣的酷炫的效果,但是怎么才能實現這些動畫的組合,這就不得不提到AnimationSet了。
(4) AnimationSet分析
- AnimationSet是動畫集合,用于組合運行多個動畫,僅支持playTogether模式。
- AnimationSet繼承了Animation的字段,但是字段的應用有一些變化:
- duration, repeatMode, fillBefore, fillAfter:這些屬性會傳遞應用到所有的子Animation
- repeatCount, fillEnabled:這些屬性在AnimationSet中不被應用
- startOffset, shareInterpolator:這些屬性僅用于AnimationSet,不會傳遞至子Animation
- 4.0以前在xml中設置duration, repeatMode, fillBefore, fillAfter, startOffset不會被應用,但是4.0之后再xml中設定這些屬性跟運行時設定效果一致
- 一些值的計算邏輯:
- duration:
- 缺省時,取所有子Animation中最長的duration;
- 已設定時,返回mDuration
- hasAlpha、willChangeTransformationMatrix、willChangeBounds:當有子Animation時,所有子Animation的值取“或”
- startTime:取所有子Animation中最小的startTime
- 子Animation中startOffset處理:
- 保存子Animation的原始startOffset
- 設置子Animation的startOffset為原始startOffset與AnimationSet的startOffset之和
- 保存的原始startOffset在AnimationSet.clear是用于恢復各子Animation的startOffset
- duration:
- applyTransformation函數實現
- 順序調用子Animation的applyTransformation,然后利用Transformation.compose組合所有子Animation返回的Transformation作為該AnimationSet當前幀的變換狀態
- started及more值取所有子Animation對應值的“或”
- ended值取所有子Animation對應值的“與”
- 當started第一次為true時,調用AnimationSet的mListener.onAnimationStart
- 當ended第一次為true(此時所有子Animation均結束)時,調用AnimationSet的mListener.onAnimationEnd
介紹完了主流動畫以及組合動畫,是不是Animation就介紹完了?其實不然,里面還漏掉了一個重要角色,那就是計算得到的動畫數據是用什么存儲的。實際上,Animation的動畫函數getTransformation
目的在于生成當前幀的一個Transformation,這個Transformation采用alpha以及Matrix存儲了一幀動畫的數據,Transformation包含兩種模式:
- alpha模式:用于支持透明度動畫
- matrix模式:用于支持縮放、平移以及旋轉動畫
同時,Transformation還提供了許多兩個接口用于組合多個Transformation:
- compose:前結合(alpha相乘、矩陣右乘、邊界疊加)
- postCompose:后結合(alpha相乘、矩陣左乘、邊界疊加)
至此,Animation本身算介紹完整了,還差一個可用于從XML中構建動畫以及插間器的AnimationUtils,這里就不做具體分析了,有興趣的同學可以自行研究。但是,到現在為止,我們還沒講明白是:getTransformation
這個函數究竟是在哪里調用的?計算得到的動畫數據又是怎么被應用的?慌不要慌,待我娓娓道來,當這些問題揭秘之后,我們就知道為什么Animation這個包要放在android.view下面以及Animation完成之后為什么View本身的屬性不會被改變,于是也就知道插間動畫(Animation)跟屬性動畫(Animator)本質上的區別在哪了。
4. Animation的調用
要了解Animation的調用源頭,要從Animation的基本使用View.startAnimation開始尋根溯源:
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
通過invalidate(true)函數會觸發View的重新繪制,由于View的繪制流程并不是本文的重點,因此這里僅說明從View.draw是怎么走到對Animation的處理函數的:
View.draw(Canvas)
—> ViewGroup.dispatchDraw(Canvas)
—> ViewGroup.drawChild(Canvas, View, long)
—> View.draw(Canvas, ViewGroup, long)
—> View.applyLegacyAnimation(ViewGroup, long, Animation, boolean)
而View.applyLegacyAnimation
就是Animation大顯神通的舞臺,其核心代碼主要分三個部分:
-
初始化Animation(僅初始化一次)
- 調用
Animation.initialize(width, height, parentWidth, parentHeight)
,通過View及ParentView的Size來解析Animation中的相關數據; - 調用
Animation.initializeInvalidateRegion(left, top, right, bottom)
來設定動畫的初始區域,并在fillBefore為true時計算Animation動畫進度為0.0f的數據
- 調用
調用
getTransformation
根據當前繪制事件生成Animation中對應幀的動畫數據-
根據動畫數據設定重繪制區域
- 若僅為Alpha動畫,此時動畫區域為View的當前區域,且不會產生變化
- 若包含非Alpha動畫,此時動畫區域需要調用
Animation.getInvalidateRegion
進行計算,該函數會根據上述生成動畫數據Thransformation
中的Matrix進行計算,并與之前的動畫區域執行unio操作,從而獲取動畫的完整區域 - 調用
ViewGroup.invalidate(int l, int t, int r, int b)
設定繪制區域
當View.applyLegacyAnimation
調用完成之后,View此次繪制的動畫數據就構建完成,之后便回到View.draw(Canvas, ViewGroup, long)
應用動畫數據對視圖進行繪制刷新,其核心代碼如下:
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
// 應用動畫數據
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
canvas.translate(-transX, -transY);
// 應用動畫數據
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
// 應用動畫數據
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
重點來了,大家看到Animation產生的動畫數據實際并不是應用在View本身的,而是應用在RenderNode或者Canvas上的,這就是為什么Animation不會改變View的屬性的根本所在。另一方面,我們知道Animation僅在View被繪制的時候才能發揮自己的價值,這也是為什么插間動畫被放在Android.view包內,因為它跟View是真心相愛的。
文章到這,其實差不多可以結束了,但是創作動畫過程中總是會被用到的一個神器還沒出現,這讓我有些不舍,盡管有太多人講解這一神器,但是我還是毅然決然地決定抄一遍書,一來表示我對這一神器的愛,另一方面也是希望讓文章更完整。
5. 插間器(Interpolator)
如果沒有插間器,Animation應該按照時間來線性計算每一個時間點的動畫幀數據;當時當加入插件器之后,我們計算動畫幀數據時就可以更加的富有創造力,我可以隨心所欲地計算任一時間點的動畫幀數據,可以新加速在減速,也可以先減速在加速,總之一句話,我的地盤我做主。按照劇情的發展,接下來我應該介紹常用插間器了,但是作為一個有態度的程序員,我是不會按常理出牌的,想要了解常用插間器的實現原理,建議閱讀Android Animations Tutorial 5: More on Interpolators。
6. 后記
其實很早之前就看過Animation的源碼,但是當時因為懶并沒有寫文章做筆記,這次因為項目需要優化動畫,于是又重新擼了一遍,在此撰文為記,以備后用。當然,也希望這篇分享能給大家一些收獲,非常感謝你的閱讀,如果有浪費到你的時間,也就浪費了,權當看了一章湊字數的小說,233333~~~