1. 前言
作為Android程序員兰粉,或者是想要去模仿一些酷炫的效果秀又,或者是為了實(shí)現(xiàn)視覺的變態(tài)需求,或者是壓抑不住內(nèi)心的創(chuàng)造欲想要炫技系吩,我們不可避免地需要做各種動(dòng)畫。Android中妒蔚,動(dòng)畫主要分為幀動(dòng)畫穿挨、插間動(dòng)畫以及屬性動(dòng)畫。幀動(dòng)畫最為簡(jiǎn)單肴盏,是用一系列的素材作為關(guān)鍵幀逐幀播放科盛,常用于制作加載動(dòng)畫,其工作量主要在設(shè)計(jì)部分菜皂;插間動(dòng)畫與屬性動(dòng)畫則更多地是需要開發(fā)通過控制各種動(dòng)畫參數(shù)來實(shí)現(xiàn)贞绵,只有系統(tǒng)地理解Android中動(dòng)畫運(yùn)行的原理,才能創(chuàng)作出更出色的動(dòng)畫恍飘,屬性動(dòng)畫在下一篇文章中分析榨崩,本文主要分享我在探索插間動(dòng)畫運(yùn)行原理過程中的一些收獲谴垫,包括:Matrix如何控制動(dòng)畫參數(shù);動(dòng)畫中各參數(shù)具體起什么作用母蛛;透明度動(dòng)畫翩剪、縮放動(dòng)畫、平移動(dòng)畫以及旋轉(zhuǎn)動(dòng)畫的運(yùn)行邏輯彩郊;動(dòng)畫在View的繪制過程中如何被應(yīng)用前弯。
2. Matrix介紹
在Android中,Matrix是一個(gè)3 x 3的矩陣:
Matrix可將一個(gè)點(diǎn)映射到另一個(gè)點(diǎn)焦辅,矩陣中包含了處理縮放博杖、透視以及平移的區(qū)域,從而可用于控制實(shí)現(xiàn)平移筷登、縮放剃根、旋轉(zhuǎn)等動(dòng)畫效果。強(qiáng)烈建議閱讀Android Matrix理論與應(yīng)用詳解以更深入地了解Matrix實(shí)現(xiàn)動(dòng)畫控制原理前方,這里僅摘錄其中的關(guān)鍵信息:
結(jié)論一:設(shè)對(duì)給定的圖像依次進(jìn)行了基本變化F1狈醉、F2、F3…..惠险、Fn苗傅,它們的變化矩陣分別為T1、T2班巩、T3…..渣慕、Tn,圖像復(fù)合變化的矩陣T可以表示為:T = TnTn-1…T1抱慌。
結(jié)論二:Preconcats matrix相當(dāng)于右乘矩陣逊桦,Postconcats matrix相當(dāng)于左乘矩陣。
Matrix還給我們提供了各種友好的接口來組合生成復(fù)雜的動(dòng)畫抑进,舉個(gè)例子:假如我們想要實(shí)現(xiàn)一個(gè)平移(a,b)之后旋轉(zhuǎn)(c,d)的動(dòng)畫强经,那用Matrix的實(shí)現(xiàn)代碼就是這樣的:
Matrix matrix = new Matrix();
matrix.setTranslate(a, b);
matrix.postScale(c, d);
3. Animation運(yùn)行原理分析
(1)基本屬性介紹
使用過Animation的同學(xué)對(duì)下述基本屬性應(yīng)該非常熟悉,這里為了文章完整性寺渗,特地贅述一下:
- mStartTime:動(dòng)畫實(shí)際開始時(shí)間
- mStartOffset:動(dòng)畫延遲時(shí)間
- mFillEnabled:mFillBefore及mFillAfter是否使能
- mFillBefore:動(dòng)畫結(jié)束之后是否需要進(jìn)行應(yīng)用動(dòng)畫
- mFillAfter:動(dòng)畫開始之前是否需要進(jìn)行應(yīng)用動(dòng)畫
- mDuration:?jiǎn)未蝿?dòng)畫運(yùn)行時(shí)長(zhǎng)
- mRepeatMode:動(dòng)畫重復(fù)模式(RESTART匿情、REVERSE)
- mRepeatCount:動(dòng)畫重復(fù)次數(shù)(INFINITE,直接值)
- mInterceptor:動(dòng)畫插間器
- mListener:動(dòng)畫開始信殊、結(jié)束炬称、重復(fù)回調(diào)監(jiān)聽器
雖然大部分都知道上面這些屬性怎么用,但是可能還是有一些人對(duì)這些字段為什么有這樣的作用不甚明白鸡号,于是我們就來分析一下转砖。
(2)計(jì)算動(dòng)畫數(shù)據(jù)
Animation在其getTransformation
函數(shù)被調(diào)用時(shí)會(huì)計(jì)算一幀動(dòng)畫數(shù)據(jù),而上面這些屬性基本都是在計(jì)算動(dòng)畫數(shù)據(jù)時(shí)發(fā)光發(fā)熱鲸伴,我們先看看getTransformation
函數(shù)的運(yùn)行邏輯:
- 若
startTime
為START_ON_FIRST_FRAME(值為-1)
時(shí)府蔗,將startTime
設(shè)定為curTime
- 計(jì)算當(dāng)前動(dòng)畫進(jìn)度:
normalizedTime = (curTime - (startTime + startOffset))/duration
- 若
mFillEnabled==false
:將normalisedTime
夾逼至[0.0f, 1.0f] - 判斷是否需要計(jì)算動(dòng)畫數(shù)據(jù):
- 若
normalisedTime
在[0.0f, 1.0f],需計(jì)算動(dòng)畫數(shù)據(jù) - 若
normalisedTime
不在[0.0f, 1.0f]:-
normalisedTime<0.0f
, 僅當(dāng)mFillBefore==true
時(shí)才計(jì)算動(dòng)畫數(shù)據(jù) -
normalisedTime>1.0f
, 僅當(dāng)mFillAfter==true
時(shí)才計(jì)算動(dòng)畫數(shù)據(jù)
-
- 若
- 若需需要計(jì)算動(dòng)畫數(shù)據(jù):
- 若當(dāng)前為第一幀動(dòng)畫汞窗,觸發(fā)
mListener.onAnimationStart
- 若
mFillEnabled==false
:將normalisedTime
夾逼至[0.0f, 1.0f] - 根據(jù)插間器
mInterpolator
調(diào)整動(dòng)畫進(jìn)度:
interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
- 若動(dòng)畫反轉(zhuǎn)標(biāo)志位
mCycleFlip
為true
姓赤,則
interpolatedTime = 1.0 - normalizedTime
- 調(diào)用動(dòng)畫更新函數(shù)
applyTransformation(interpolatedTime, transformation)
計(jì)算出動(dòng)畫數(shù)據(jù)
- 若當(dāng)前為第一幀動(dòng)畫汞窗,觸發(fā)
- 若夾逼之前
normalisedTime
大于1.0f, 則判斷是否需繼續(xù)執(zhí)行動(dòng)畫:- 已執(zhí)行次數(shù)
mRepeatCount
等于需執(zhí)行次數(shù)mRepeated
- 若未觸發(fā)
mListener.onAnimationEnd
,則觸發(fā)之
- 若未觸發(fā)
- 已執(zhí)行次數(shù)
mRepeatCount
不等于需執(zhí)行次數(shù)mRepeated
- 自增
mRepeatCount
- 重置
mStartTime
為-1 - 若
mRepeatMode
為REVERSE
仲吏,則取反mCycleFlip
- 觸發(fā)
mListener.onAnimationRepeat
- 自增
- 已執(zhí)行次數(shù)
這一段是根據(jù)getTransformation
源碼分析出來的不铆,建議有興趣的同學(xué)可以直接查看源碼。上面這段分析留了一個(gè)不小的懸念裹唆,那就是動(dòng)畫更新函數(shù)是什么鬼誓斥,這個(gè)函數(shù)在Animation這個(gè)抽象類中僅僅是個(gè)鉤子函數(shù),由其子類提供具體實(shí)現(xiàn)许帐,于是自然而然地引出了我們的下一個(gè)主題:主流動(dòng)畫介紹劳坑。
(3)主流動(dòng)畫分析
-
AlphaAnimation:透明度動(dòng)畫
- 基本屬性
- mFromAlpha:起始透明度
- mToAlpha:終止透明度
- applyTransformation函數(shù)實(shí)現(xiàn)
- transformation.setAlpha(mFromAlpha + ((mToAlpha - mFromAlpha) * interpolatedTime))
- 基本屬性
-
ScaleAnimation:縮放動(dòng)畫
- 基本屬性
- mFromX:起始X值
- mToX:終止X值
- mFromY:起始Y值
- mToY:終止Y值
- mPivotX:縮放中心點(diǎn)X坐標(biāo)
- mPivotY:縮放中心點(diǎn)Y坐標(biāo)
- 屬性計(jì)算邏輯
- mFromX、mToX成畦、mFromY距芬、mToY計(jì)算
- Float類型scale直接值
- Faction類型相對(duì)值
- 相對(duì)于自身(%):百分比轉(zhuǎn)換為float直接值
- 相對(duì)于父親(%p):根據(jù)父親size計(jì)算出size直接值,然后計(jì)算與本身size的百分比循帐,最后轉(zhuǎn)換為float直接值
- Dimension類型size直接值:計(jì)算與本身size的百分比框仔,然后轉(zhuǎn)換為float直接值
- mPivotX、mPivotY計(jì)算
- ABSOLUTE類型直接值
- RELATIVE_TO_SELF類型相對(duì)值:相對(duì)值乘以自身size得到直接值
- RELATIVE_TO_PARENT類型相對(duì)值:相對(duì)值乘以父親size得到直接值
- mFromX、mToX成畦、mFromY距芬、mToY計(jì)算
- applyTransformation函數(shù)實(shí)現(xiàn)
- sx = mFromX + ((mToX - mFromX) * interpolatedTime)
- sy = mFromY + ((mToY - mFromY) * interpolatedTime)
- 是否設(shè)定縮放中心點(diǎn):
- 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
- 否則:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
- 基本屬性
-
TranslateAnimation:平移動(dòng)畫
- 基本屬性
- mFromXDelta
- mToXDelta
- mFromYDelta
- mToYDelta
- 屬性計(jì)算邏輯
- 同ScaleAnimation中mPivotX拄养、mPivotY的計(jì)算邏輯
- applyTransformation函數(shù)實(shí)現(xiàn)
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime)
- dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime)
- transformation.getMatrix().setTranslate(dx, dy)
- 基本屬性
-
RotateAnimation:旋轉(zhuǎn)動(dòng)畫
- 基本屬性
- mFromDegrees
- mToDegrees
- mPivotX
- mPivotY
- 屬性計(jì)算邏輯
- mFromDegrees离斩、mToDegrees均為角度(°)絕對(duì)值
- mPivotX、mPivotY計(jì)算邏輯同ScaleAnimation
- applyTransformation函數(shù)實(shí)現(xiàn)
- 是否設(shè)定縮放中心點(diǎn):
- 若mPivotX==0 且 mPivotY==0:transformation.getMatrix().setScale(sx, sy)
- 否則:transformation.getMatrix().setScale(sx, sy, mPivotX, mPivotY)
- 是否設(shè)定縮放中心點(diǎn):
- 基本屬性
透明度瘪匿、縮放跛梗、平移以及旋轉(zhuǎn)是最基本的動(dòng)畫,通過組合這些動(dòng)畫可以實(shí)現(xiàn)各種不一樣的酷炫的效果柿顶,但是怎么才能實(shí)現(xiàn)這些動(dòng)畫的組合茄袖,這就不得不提到AnimationSet了。
(4) AnimationSet分析
- AnimationSet是動(dòng)畫集合嘁锯,用于組合運(yùn)行多個(gè)動(dòng)畫宪祥,僅支持playTogether模式。
- AnimationSet繼承了Animation的字段家乘,但是字段的應(yīng)用有一些變化:
- duration, repeatMode, fillBefore, fillAfter:這些屬性會(huì)傳遞應(yīng)用到所有的子Animation
- repeatCount, fillEnabled:這些屬性在AnimationSet中不被應(yīng)用
- startOffset, shareInterpolator:這些屬性僅用于AnimationSet蝗羊,不會(huì)傳遞至子Animation
- 4.0以前在xml中設(shè)置duration, repeatMode, fillBefore, fillAfter, startOffset不會(huì)被應(yīng)用,但是4.0之后再xml中設(shè)定這些屬性跟運(yùn)行時(shí)設(shè)定效果一致
- 一些值的計(jì)算邏輯:
- duration:
- 缺省時(shí)仁锯,取所有子Animation中最長(zhǎng)的duration耀找;
- 已設(shè)定時(shí),返回mDuration
- hasAlpha、willChangeTransformationMatrix野芒、willChangeBounds:當(dāng)有子Animation時(shí)蓄愁,所有子Animation的值取“或”
- startTime:取所有子Animation中最小的startTime
- 子Animation中startOffset處理:
- 保存子Animation的原始startOffset
- 設(shè)置子Animation的startOffset為原始startOffset與AnimationSet的startOffset之和
- 保存的原始startOffset在AnimationSet.clear是用于恢復(fù)各子Animation的startOffset
- duration:
- applyTransformation函數(shù)實(shí)現(xiàn)
- 順序調(diào)用子Animation的applyTransformation,然后利用Transformation.compose組合所有子Animation返回的Transformation作為該AnimationSet當(dāng)前幀的變換狀態(tài)
- started及more值取所有子Animation對(duì)應(yīng)值的“或”
- ended值取所有子Animation對(duì)應(yīng)值的“與”
- 當(dāng)started第一次為true時(shí)狞悲,調(diào)用AnimationSet的mListener.onAnimationStart
- 當(dāng)ended第一次為true(此時(shí)所有子Animation均結(jié)束)時(shí)撮抓,調(diào)用AnimationSet的mListener.onAnimationEnd
介紹完了主流動(dòng)畫以及組合動(dòng)畫,是不是Animation就介紹完了摇锋?其實(shí)不然丹拯,里面還漏掉了一個(gè)重要角色,那就是計(jì)算得到的動(dòng)畫數(shù)據(jù)是用什么存儲(chǔ)的荸恕。實(shí)際上乖酬,Animation的動(dòng)畫函數(shù)getTransformation
目的在于生成當(dāng)前幀的一個(gè)Transformation,這個(gè)Transformation采用alpha以及Matrix存儲(chǔ)了一幀動(dòng)畫的數(shù)據(jù)融求,Transformation包含兩種模式:
- alpha模式:用于支持透明度動(dòng)畫
- matrix模式:用于支持縮放咬像、平移以及旋轉(zhuǎn)動(dòng)畫
同時(shí),Transformation還提供了許多兩個(gè)接口用于組合多個(gè)Transformation:
- compose:前結(jié)合(alpha相乘双肤、矩陣右乘施掏、邊界疊加)
- postCompose:后結(jié)合(alpha相乘、矩陣左乘茅糜、邊界疊加)
至此七芭,Animation本身算介紹完整了,還差一個(gè)可用于從XML中構(gòu)建動(dòng)畫以及插間器的AnimationUtils蔑赘,這里就不做具體分析了狸驳,有興趣的同學(xué)可以自行研究。但是缩赛,到現(xiàn)在為止耙箍,我們還沒講明白是:getTransformation
這個(gè)函數(shù)究竟是在哪里調(diào)用的?計(jì)算得到的動(dòng)畫數(shù)據(jù)又是怎么被應(yīng)用的酥馍?慌不要慌辩昆,待我娓娓道來,當(dāng)這些問題揭秘之后旨袒,我們就知道為什么Animation這個(gè)包要放在android.view下面以及Animation完成之后為什么View本身的屬性不會(huì)被改變汁针,于是也就知道插間動(dòng)畫(Animation)跟屬性動(dòng)畫(Animator)本質(zhì)上的區(qū)別在哪了。
4. Animation的調(diào)用
要了解Animation的調(diào)用源頭砚尽,要從Animation的基本使用View.startAnimation開始尋根溯源:
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
通過invalidate(true)函數(shù)會(huì)觸發(fā)View的重新繪制施无,由于View的繪制流程并不是本文的重點(diǎn),因此這里僅說明從View.draw是怎么走到對(duì)Animation的處理函數(shù)的:
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大顯神通的舞臺(tái)必孤,其核心代碼主要分三個(gè)部分:
-
初始化Animation(僅初始化一次)
- 調(diào)用
Animation.initialize(width, height, parentWidth, parentHeight)
猾骡,通過View及ParentView的Size來解析Animation中的相關(guān)數(shù)據(jù); - 調(diào)用
Animation.initializeInvalidateRegion(left, top, right, bottom)
來設(shè)定動(dòng)畫的初始區(qū)域,并在fillBefore為true時(shí)計(jì)算Animation動(dòng)畫進(jìn)度為0.0f的數(shù)據(jù)
- 調(diào)用
調(diào)用
getTransformation
根據(jù)當(dāng)前繪制事件生成Animation中對(duì)應(yīng)幀的動(dòng)畫數(shù)據(jù)-
根據(jù)動(dòng)畫數(shù)據(jù)設(shè)定重繪制區(qū)域
- 若僅為Alpha動(dòng)畫兴想,此時(shí)動(dòng)畫區(qū)域?yàn)閂iew的當(dāng)前區(qū)域幢哨,且不會(huì)產(chǎn)生變化
- 若包含非Alpha動(dòng)畫,此時(shí)動(dòng)畫區(qū)域需要調(diào)用
Animation.getInvalidateRegion
進(jìn)行計(jì)算襟企,該函數(shù)會(huì)根據(jù)上述生成動(dòng)畫數(shù)據(jù)Thransformation
中的Matrix進(jìn)行計(jì)算嘱么,并與之前的動(dòng)畫區(qū)域執(zhí)行unio操作狮含,從而獲取動(dòng)畫的完整區(qū)域 - 調(diào)用
ViewGroup.invalidate(int l, int t, int r, int b)
設(shè)定繪制區(qū)域
當(dāng)View.applyLegacyAnimation
調(diào)用完成之后顽悼,View此次繪制的動(dòng)畫數(shù)據(jù)就構(gòu)建完成,之后便回到View.draw(Canvas, ViewGroup, long)
應(yīng)用動(dòng)畫數(shù)據(jù)對(duì)視圖進(jìn)行繪制刷新几迄,其核心代碼如下:
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
// 應(yīng)用動(dòng)畫數(shù)據(jù)
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
canvas.translate(-transX, -transY);
// 應(yīng)用動(dòng)畫數(shù)據(jù)
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
// 應(yīng)用動(dòng)畫數(shù)據(jù)
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
重點(diǎn)來了蔚龙,大家看到Animation產(chǎn)生的動(dòng)畫數(shù)據(jù)實(shí)際并不是應(yīng)用在View本身的,而是應(yīng)用在RenderNode或者Canvas上的映胁,這就是為什么Animation不會(huì)改變View的屬性的根本所在木羹。另一方面,我們知道Animation僅在View被繪制的時(shí)候才能發(fā)揮自己的價(jià)值解孙,這也是為什么插間動(dòng)畫被放在Android.view包內(nèi)坑填,因?yàn)樗鶹iew是真心相愛的。
文章到這弛姜,其實(shí)差不多可以結(jié)束了脐瑰,但是創(chuàng)作動(dòng)畫過程中總是會(huì)被用到的一個(gè)神器還沒出現(xiàn),這讓我有些不舍廷臼,盡管有太多人講解這一神器苍在,但是我還是毅然決然地決定抄一遍書,一來表示我對(duì)這一神器的愛荠商,另一方面也是希望讓文章更完整寂恬。
5. 插間器(Interpolator)
如果沒有插間器,Animation應(yīng)該按照時(shí)間來線性計(jì)算每一個(gè)時(shí)間點(diǎn)的動(dòng)畫幀數(shù)據(jù)莱没;當(dāng)時(shí)當(dāng)加入插件器之后初肉,我們計(jì)算動(dòng)畫幀數(shù)據(jù)時(shí)就可以更加的富有創(chuàng)造力,我可以隨心所欲地計(jì)算任一時(shí)間點(diǎn)的動(dòng)畫幀數(shù)據(jù)饰躲,可以新加速在減速牙咏,也可以先減速在加速,總之一句話属铁,我的地盤我做主眠寿。按照劇情的發(fā)展,接下來我應(yīng)該介紹常用插間器了焦蘑,但是作為一個(gè)有態(tài)度的程序員盯拱,我是不會(huì)按常理出牌的,想要了解常用插間器的實(shí)現(xiàn)原理,建議閱讀Android Animations Tutorial 5: More on Interpolators狡逢。
6. 后記
其實(shí)很早之前就看過Animation的源碼宁舰,但是當(dāng)時(shí)因?yàn)閼胁]有寫文章做筆記,這次因?yàn)轫?xiàng)目需要優(yōu)化動(dòng)畫奢浑,于是又重新擼了一遍蛮艰,在此撰文為記,以備后用雀彼。當(dāng)然壤蚜,也希望這篇分享能給大家一些收獲,非常感謝你的閱讀徊哑,如果有浪費(fèi)到你的時(shí)間袜刷,也就浪費(fèi)了,權(quán)當(dāng)看了一章湊字?jǐn)?shù)的小說莺丑,233333~~~