Android Animation運行原理詳解

1. 前言

作為Android程序員,或者是想要去模仿一些酷炫的效果,或者是為了實現視覺的變態需求,或者是壓抑不住內心的創造欲想要炫技,我們不可避免地需要做各種動畫。Android中,動畫主要分為幀動畫、插間動畫以及屬性動畫。幀動畫最為簡單,是用一系列的素材作為關鍵幀逐幀播放,常用于制作加載動畫,其工作量主要在設計部分;插間動畫與屬性動畫則更多地是需要開發通過控制各種動畫參數來實現,只有系統地理解Android中動畫運行的原理,才能創作出更出色的動畫,屬性動畫在下一篇文章中分析,本文主要分享我在探索插間動畫運行原理過程中的一些收獲,包括:Matrix如何控制動畫參數;動畫中各參數具體起什么作用;透明度動畫、縮放動畫、平移動畫以及旋轉動畫的運行邏輯;動畫在View的繪制過程中如何被應用。

2. Matrix介紹

在Android中,Matrix是一個3 x 3的矩陣:

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函數的運行邏輯:

  1. startTimeSTART_ON_FIRST_FRAME(值為-1)時,將startTime設定為curTime
  2. 計算當前動畫進度:
    normalizedTime = (curTime - (startTime + startOffset))/duration
  3. mFillEnabled==false:將normalisedTime夾逼至[0.0f, 1.0f]
  4. 判斷是否需要計算動畫數據:
    • normalisedTime在[0.0f, 1.0f],需計算動畫數據
    • normalisedTime不在[0.0f, 1.0f]:
      • normalisedTime<0.0f, 僅當mFillBefore==true時才計算動畫數據
      • normalisedTime>1.0f, 僅當mFillAfter==true時才計算動畫數據
  5. 若需需要計算動畫數據:
    • 若當前為第一幀動畫,觸發mListener.onAnimationStart
    • mFillEnabled==false:將normalisedTime夾逼至[0.0f, 1.0f]
    • 根據插間器mInterpolator調整動畫進度:
      interpolatedTime = mInterpolator.getInterpolation(normalizedTime)
    • 若動畫反轉標志位mCycleFliptrue,則
      interpolatedTime = 1.0 - normalizedTime
    • 調用動畫更新函數applyTransformation(interpolatedTime, transformation)計算出動畫數據
  6. 若夾逼之前normalisedTime大于1.0f, 則判斷是否需繼續執行動畫:
    • 已執行次數mRepeatCount等于需執行次數mRepeated
      • 若未觸發mListener.onAnimationEnd,則觸發之
    • 已執行次數mRepeatCount不等于需執行次數mRepeated
      • 自增mRepeatCount
      • 重置mStartTime為-1
      • mRepeatModeREVERSE,則取反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得到直接值
    • 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
  • 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大顯神通的舞臺,其核心代碼主要分三個部分:

  1. 初始化Animation(僅初始化一次)

    • 調用Animation.initialize(width, height, parentWidth, parentHeight),通過View及ParentView的Size來解析Animation中的相關數據;
    • 調用Animation.initializeInvalidateRegion(left, top, right, bottom)來設定動畫的初始區域,并在fillBefore為true時計算Animation動畫進度為0.0f的數據
  2. 調用getTransformation根據當前繪制事件生成Animation中對應幀的動畫數據

  3. 根據動畫數據設定重繪制區域

    • 若僅為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~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容