文章出處

本文結合一些周知的概念和源碼片段,對View動畫的工作原理進行挖掘和分析。以下不是對源碼一絲不茍的分析過程,只是以搞清楚Animation的執行過程、如何被周期性調用為目標粗略分析下相關方法的執行細節,最終貫穿View動畫實際發生的一切。

View動畫使用

Android提供了屬性動畫(property animation)、幀動畫(frame-by-frame animation)和View動畫( tweened animation:補間動畫),View動畫的使用相對簡單,但又不像真正意義上的動畫那樣對View產生有效的影響。
在從某些方面開始分析View動畫的工作原理之前,先來回顧一下使用View動畫相關的細節。

step 1. 準備Animation對象

可以通過xml或者代碼生成一個Animation對象。
通常是建議使用xml來定義動畫的,這樣有更好的閱讀性、獨立性和復用性,不過最終的,通過AnimationUtils.loadAnimation()加載一個xml動畫以后,得到的就是一個Animation對象。
不同動畫對象有它的專有參數需要設置,一般的,需要設置一些所有動畫公共的重要屬性,對應方法如setFillAfter、setDuration、setRepeatMode和setInterpolator等。
例如下面的代碼創建了一個在600ms內沿著Y軸向下移動300像素的位移動畫:

TranslateAnimation anim = new TranslateAnimation(0, 0, 0, 300);
anim.setDuration(600);
anim.setFillAfter(true);

step 2. 執行動畫

通常會調用View.startAnimation立即開啟動畫,它里面完成了開啟動畫需要的兩個關鍵操作。另一種方式是通過View.setAnimation方法。下面是兩個方法的原型:

/**
 * Start the specified animation now.
 *
 * @param animation the animation to start now
 */
public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}

/**
 * Sets the next animation to play for this view.
 * If you want the animation to play immediately, use
 * {@link #startAnimation(android.view.animation.Animation)} instead.
 * This method provides allows fine-grained
 * control over the start time and invalidation, but you
 * must make sure that 1) the animation has a start time set, and
 * 2) the view's parent (which controls animations on its children)
 * will be invalidated when the animation is supposed to
 * start.
 *
 * @param animation The next animation, or null.
 */
 public void setAnimation(Animation animation) {
   mCurrentAnimation = animation;
   if (animation != null) {
       // If the screen is off assume the animation start time is now instead of
       // the next frame we draw. Keeping the START_ON_FIRST_FRAME start time
       // would cause the animation to start when the screen turns back on
       if (mAttachInfo != null && !mAttachInfo.mScreenOn &&
               animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
           animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
       }
       animation.reset();
   }
}

從方法setAnimation的說明可以看到,為了開始動畫的執行,需要為動畫設置開始時間,并保證目標view對象的父View的繪制內容被指定為失效——簡單些就是父view執行一次invalidate。
animation.setStartTime(Animation.START_ON_FIRST_FRAME)的執行使得“之后第一次調用”animation.getTransformation方法時動畫即開始(后面會詳細講到方法getTransformation,這里有個印象即可),這樣的效果就是設置了animation動畫的啟動時間為“當前”。方法startAnimation中執行setAnimation后繼續調用了invalidateParentCaches()、invalidate(true),目的正是完成開始動畫的要求:“make view's parent invalidated”。

view.startAnimation會把對應animation對象的開始時間設置為“當前時間”,這樣動畫就立即開始了。也可以像下面這樣指定動畫開始時間來延遲或者立即執行動畫:

((View) view.getParent()).invalidate();
// 延遲2s執行動畫
mMoveAnim.setStartTime(AnimationUtils.currentAnimationTimeMillis() + 2000);
view.setAnimation(mMoveAnim);

View動畫的“不變性”

根據介紹,View動畫的執行最終并不影響View對象本身的“放置區域”。
例如對一個按鈕執行TranslateAnimation動畫,將它“移動到另一個位置”,那么新位置是無法點擊的,而原始按鈕的位置依然可以點擊。
這是為什么呢?
作為結論,View動畫只是針對View繪制的內容進行各種變換,但并不影響View對象本身的布局屬性。也就是View動畫只是改變了對應View得到的父布局的Canvas上繪制的內容,但沒有新的layout過程,View的left,top,right,bottom都沒改變。

View動畫相關類型

類Animation是所有View動畫的抽象基類,它的子類有 AlphaAnimation, AnimationSet, RotateAnimation, ScaleAnimation, TranslateAnimation等。為了完成功能,Animation組合了其它幾個類,下面依次了解下。

Animation類

Animation封裝了動畫的參數、動畫執行邏輯、動畫運行狀態的有關數據。它的工作職責類似android.widget.Scroller類,如果不了解Scroller,下面是對它的一個近似比擬:
假設把Scroller比作一個“自由落體運動計算器”,你可以為它設置起始高度,比如50m,重力g,比如9.8。它封裝了自由落體運動的計算規則,然后提供了一個方法doubole getHeight(float seconds)用來根據傳遞的秒數得到對應物體的高度。如果需要根據這個類來在界面上顯示一個小球下落的動畫過程,那么關于時間的倒計時,什么時候去調用getHeight等已經是使用者的事情了。

Animation正是類似的設計理念,當然是針對動畫而言。概括的講,Animation封裝了有關要執行的動畫的配置信息,提供了動畫運行的控制方法。最主要的,提供方法供調用者在特定時間點獲得動畫執行的結果。

Animation、Scroller的這種設計思路在Android UI框架中很有用,實現了數據、邏輯、執行過程獨立封裝。

1. 設置動畫開始時間

方法setStartTime用來設置動畫開始時間:

public setStartTime(long startTimeMillis);

如果是參數是常量START_ON_FIRST_FRAME(雖然不應該去關心,提醒一下它的值是負數:-1),那么表示動畫開始時間就是“當前”。這樣以后,在Animation的使用者第一次調用其getTransformation方法獲得動畫結果時,計算動畫經過的實際毫秒數所使用的動畫開始時間就被記為方法調用時的當前時間的毫秒數。

使用-1這樣的負數來表示特殊狀態的用法已經很平常了,比如indexOf這樣的方法,-1表示未選擇列表中任何一項等。

動畫相關的“當前時間”統一是通過下面的AnimationUtils提供的工具方法獲得的:

/**
 * Returns the current animation time in milliseconds. This time should be used when invoking
 * {@link Animation#setStartTime(long)}. Refer to {@link android.os.SystemClock} for more
 * information about the different available clocks. The clock used by this method is
 * <em>not</em> the "wall" clock (it is not {@link System#currentTimeMillis}).
 *
 * @return the current animation time in milliseconds
 *
 * @see android.os.SystemClock
 */
public static long currentAnimationTimeMillis() {
    return SystemClock.uptimeMillis();
}

實際上,有關動畫的時間點和持續時間等,是不需要和實際時間有聯系的,因為相關的計算關鍵就是毫秒單位的時間差計算。

2. 設置持續時間

動畫的一項重要屬性就是其duration,它影響動畫執行的時間。如果沒有重復屬性的設置,動畫重復次數就是1次,動畫開始被多次調用“獲得動畫結果”時,只有發現動畫時間超過了startTime + duration后,就過期/結束不再執行了。

3. 重復動畫

動畫的重復包括重復次數和重復類型(RESTART/REVERSE)。動畫最終執行的持續時間為repeatCount * duration的毫秒數。

4. 運行前后的動畫結果的應用

如果希望動畫開始和結束后依然應用動畫的執行結果,調用setFillEnabled、setFillBefore、setFillAfter來完成這樣的目標。這在應用多個動畫時(AnimationSet)需要特別關注。

5. 獲得動畫結果

當前動畫開始后,其它類以周期性調用它的getTransformation方法來不斷獲得動畫結果。方法的返回值指示當前動畫是否結束。原型如下:

/**
 * Gets the transformation to apply at a specified point in time. Implementations of this
 * method should always replace the specified Transformation or document they are doing
 * otherwise.
 *
 * @param currentTime Where we are in the animation. This is wall clock time.
 * @param outTransformation A transformation object that is provided by the
 *        caller and will be filled in by the animation.
 * @param scale Scaling factor to apply to any inputs to the transform operation, such
 *        pivot points being rotated or scaled around.
 * @return True if the animation is still running
 */
public boolean getTransformation(long currentTime, Transformation outTransformation,
        float scale);

這個方法完成了整個View動畫工作流程最重要的公共部分的功能。
首先看下它的參數:

  • currentTime
    currentTime是和動畫開始時間相對應的“當前時間”,獲得動畫的執行結果必需的參數就是動畫已進行時間,即currentTime - startTime。
  • outTransformation
    outTransformation用來存儲動畫的結果,View動畫的設計目標就是是關于View的2D內容的各種幾何變換,后面會對Transformation稍作介紹。

方法getTransformation主要完成以下任務:

  1. 根據傳遞的currentTime得到距離動畫開始經過的時間normalizedTime,它表示動畫的規范化時間(進度),它是一個 0 和 1 之間的值。其計算公式大致是normalizedTime = (currentTime - startTime) / duration,如果動畫還需要重復,則開始時間被重置為START_ON_FIRST_FRAME,以便重新開始動畫。

  2. 計算動畫是否過期。當重復次數達到最大次數,且normalizedTime大于1f,表示當前動畫運行結束。不再有后續動畫執行。方法的返回值指示了這個含義。

  3. 如果動畫還需要運行,就調用留給子類重寫的方法applyTransformation完成動畫邏輯。

再次強調,方法getTransformation的返回值表示當前動畫的運行是否結束,返回值的計算并不復雜,就是根據重復次數,已經過時間得出是否繼續執行動畫。這里如果重寫方法getTransformation讓它一直返回true,那么動畫也就一直不會停止了。在后面對方法getTransformation的調用棧的分析中會弄清楚這里返回值對動畫的調用持續次數的影響過程。

6. 動畫邏輯

每個Animation動畫對View執行的變換操作——也就是動畫操作是不同的,所以Animation定義了方法applyTransformation來供子類完成實際的動畫邏輯。類似View中draw方法和onDraw方法的關系那樣。
方法原型:

/**
 * Helper for getTransformation. Subclasses should implement this to apply
 * their transforms given an interpolation value.  Implementations of this
 * method should always replace the specified Transformation or document
 * they are doing otherwise.
 *
 * @param interpolatedTime The value of the normalized time (0.0 to 1.0)
 *        after it has been run through the interpolation function.
 * @param t The Transformation object to fill in with the current
 *        transforms.
 */
protected void applyTransformation(float interpolatedTime, Transformation outTransformation)

參數interpolatedTime是根據前面getTransformation中計算得到的normalizedTime(進度時間)經過插值算法轉換后的“插值時間”。插值時間最終表示了動畫應該執行到的程度。
所有Animation子類都在applyTransformation中執行其專有的動畫邏輯。比如位移動畫執行outTransformation.getMatrix().setTranslate(dx, dy)設置移動的距離,距離dx、dy的計算就是根據interpolatedTime計算而來。最后的動畫計算結果保存在參數Transformation outTransformation中。

7. 動畫運行控制

reset、cancel,initialize等方法對動畫的生命周期進行控制。主要是開始時間和一些標記字段的設置。面向對象中,對過程狀態的控制就是相應標志字段的設置。如果使用過就不會陌生。

Animation子類

類似View和具體View子類那樣的關系,Animation提供了有關View動畫的公共基礎,而Animation子類完成專有的動畫邏輯。
下面舉例AlphaAnimation(漸變動畫)的完整代碼,它實在很簡短:

public class AlphaAnimation extends Animation {
    private float mFromAlpha;
    private float mToAlpha;

    /**
     * Constructor used when an AlphaAnimation is loaded from a resource.
     *
     * @param context Application context to use
     * @param attrs Attribute set from which to read values
     */
    public AlphaAnimation(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a =
            context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AlphaAnimation);

        mFromAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_fromAlpha, 1.0f);
        mToAlpha = a.getFloat(com.android.internal.R.styleable.AlphaAnimation_toAlpha, 1.0f);

        a.recycle();
    }

    /**
     * Constructor to use when building an AlphaAnimation from code
     *
     * @param fromAlpha Starting alpha value for the animation, where 1.0 means
     *        fully opaque and 0.0 means fully transparent.
     * @param toAlpha Ending alpha value for the animation.
     */
    public AlphaAnimation(float fromAlpha, float toAlpha) {
        mFromAlpha = fromAlpha;
        mToAlpha = toAlpha;
    }

    /**
     * Changes the alpha property of the supplied {@link Transformation}
     */
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float alpha = mFromAlpha;
        t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
    }

    @Override
    public boolean willChangeTransformationMatrix() {
        return false;
    }

    @Override
    public boolean willChangeBounds() {
        return false;
    }

    @Override
    public boolean hasAlpha() {
        return true;
    }
}

看到沒有,除去幾個有關從xml獲得attributes的構造器,它唯一重寫了的關鍵方法就是applyTransformation。實際上,當需要構造一個利用Animation機制的“簡單、特殊”的動畫時,唯一需要重寫的方法就是applyTransformation——完成動畫邏輯的地方。

AnimationListener

這是Animation提供的接口,用來對動畫運行時的start、repeat和end進行監聽。

Interpolator

每個Animation會關聯一個Interpolator對象,在getTransformation方法回調子類重寫的applyTransformation方法時,它完成normalizedTime到interpolatedTime的轉換。

Transformation

包含Matrix和Alpha字段,用來記錄某次動畫計算的變換結果。

View繪制的頻率

我們最終在屏幕看到的內容,并不是像現實中照片或者紙片那樣——“連續可見”的,實際上屏幕是不斷刷新的(平常所說的顯示器的刷新頻率60Hz這樣的)。對于Android的UI系統,類似游戲引擎那樣的機制,每一幀都在執行一次繪制。app中某個界面形成的ViewTree通過遍歷每個View對象執行其draw方法(進而執行onDraw方法)來完成整個屏幕要繪制內容的計算,并且繪制的內容會被緩存,之后在沒有內容變化的情況下就向顯示設備輸出之前被緩存了的數據。invalidate方法就是用來通知整個ViewTree,當前View的內容過期,應該重新計算要繪制的內容。

下面是一個為期300ms的動畫執行一次時applyTransformation方法的調用頻率:

08-30 11:12:19.032 4174-4174/? D/TAG: call applyTransformation at [ 0 ] ms, after [ 0 ] ms
08-30 11:12:19.048 4174-4174/? D/TAG: call applyTransformation at [ 16 ] ms, after [ 16 ] ms
08-30 11:12:19.060 4174-4174/? D/TAG: call applyTransformation at [ 32 ] ms, after [ 16 ] ms
08-30 11:12:19.080 4174-4174/? D/TAG: call applyTransformation at [ 49 ] ms, after [ 17 ] ms
08-30 11:12:19.096 4174-4174/? D/TAG: call applyTransformation at [ 66 ] ms, after [ 17 ] ms
08-30 11:12:19.116 4174-4174/? D/TAG: call applyTransformation at [ 83 ] ms, after [ 17 ] ms
08-30 11:12:19.132 4174-4174/? D/TAG: call applyTransformation at [ 99 ] ms, after [ 16 ] ms
08-30 11:12:19.144 4174-4174/? D/TAG: call applyTransformation at [ 115 ] ms, after [ 16 ] ms
08-30 11:12:19.164 4174-4174/? D/TAG: call applyTransformation at [ 133 ] ms, after [ 18 ] ms
08-30 11:12:19.176 4174-4174/? D/TAG: call applyTransformation at [ 148 ] ms, after [ 15 ] ms
08-30 11:12:19.196 4174-4174/? D/TAG: call applyTransformation at [ 165 ] ms, after [ 17 ] ms
08-30 11:12:19.212 4174-4174/? D/TAG: call applyTransformation at [ 182 ] ms, after [ 17 ] ms
08-30 11:12:19.228 4174-4174/? D/TAG: call applyTransformation at [ 198 ] ms, after [ 16 ] ms
08-30 11:12:19.244 4174-4174/? D/TAG: call applyTransformation at [ 216 ] ms, after [ 18 ] ms
08-30 11:12:19.264 4174-4174/? D/TAG: call applyTransformation at [ 235 ] ms, after [ 19 ] ms
08-30 11:12:19.284 4174-4174/? D/TAG: call applyTransformation at [ 254 ] ms, after [ 19 ] ms
08-30 11:12:19.304 4174-4174/? D/TAG: call applyTransformation at [ 272 ] ms, after [ 18 ] ms
08-30 11:12:19.324 4174-4174/? D/TAG: call applyTransformation at [ 293 ] ms, after [ 21 ] ms
08-30 11:12:19.340 4174-4174/? D/TAG: call applyTransformation at [ 310 ] ms, after [ 17 ] ms

可以看到,調用的間隔保持在15-21ms的范圍,上面總共執行了19次。這里只是針對一個設備的測試數據,它反映了一個事實就是:屏幕繪制的頻率是隨設備而變化的,我們無法以固定的頻率去要求繪制的執行,比如你無法讓onDraw每秒執行10000次!!相反地,是和硬件相關的那部分系統代碼做最終的協調,對于我們的app來說,當你需要執行動畫這樣的“以某個頻率去繪制”任務時,要做的就是通知系統開始周期性調用,而對應的頻率由系統來決定——最終是一個“不斷盡快去繪制”的過程——然后每次回調時由我們來完成要繪制內容的計算。如果你自己去已某種假定的時間間隔去不斷嘗試invalidate來要求界面重繪,最終的執行頻率很容易成為“漏掉一些節拍、延遲到下一個”這樣的情況。

理解上面的觀點很重要。在不借助Animation所提供的API時,我們可以定義一個當前時間開始的300ms的倒計時,然后調用invalidate通知onDraw的執行,之后在onDraw中根據是否過期來繼續執行invalidate引發下一次onDraw的執行。此時,最終的onDraw的執行間隔依然是由系統決定的,我們只負責判斷是否退出invalidate調用的“遞歸”(從某種形式上看是的)。后面可以看到Animation機制幾乎就是類似的原理。

View動畫的執行過程分析

以上有關Animation的使用和相關類型的分析已經對View動畫做了足夠的回顧,接下來的目標就是弄清楚getTransformation方法的調用者是誰?又是如何在動畫運行期間被不斷調用的?
接下來就從applyTransformation中產生一個Throwable對象,得到有關的調用棧信息,然后分析調用棧來找到getTransformation的調用邏輯。

調用棧

applyTransformation方法一次執行過程所涉及的調用棧信息如下,詳細都不會陌生。對應SDK版本為4.4.4。高版本的代碼會有差異,但整體上有關Animation的執行過程(應該)是一致的。

call applyTransformation:
java.lang.Throwable
      at com.idlestar.androiddocs.view.widget.PieGraphView$3.applyTransformation(PieGraphView.java:167)
      at android.view.animation.Animation.getTransformation(Animation.java:870)
      at android.view.animation.Animation.getTransformation(Animation.java:940)
      at android.view.View.drawAnimation(View.java:13962)
      at android.view.View.draw(View.java:14099)
      at android.view.ViewGroup.drawChild(ViewGroup.java:3103)
      at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2940)
      at android.view.View.getDisplayList(View.java:13357)
      at android.view.View.getDisplayList(View.java:13404)
      at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3077)
      at android.view.View.getDisplayList(View.java:13300)
      at android.view.View.getDisplayList(View.java:13404)
      at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3077)
      at android.view.View.getDisplayList(View.java:13300)
      at android.view.View.getDisplayList(View.java:13404)
      at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:3077)
      at android.view.View.getDisplayList(View.java:13300)
      at android.view.View.getDisplayList(View.java:13404)
      at android.view.HardwareRenderer$GlRenderer.buildDisplayList(HardwareRenderer.java:1570)
      at android.view.HardwareRenderer$GlRenderer.draw(HardwareRenderer.java:1449)
      at android.view.ViewRootImpl.draw(ViewRootImpl.java:2377)
      at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2249)
      at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1879)
      at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:996)
      at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5600)
      at android.view.Choreographer$CallbackRecord.run(Choreographer.java:761)
      at android.view.Choreographer.doCallbacks(Choreographer.java:574)
      at android.view.Choreographer.doFrame(Choreographer.java:544)
      at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:747)
      at android.os.Handler.handleCallback(Handler.java:733)
      at android.os.Handler.dispatchMessage(Handler.java:95)
      at android.os.Looper.loop(Looper.java:136)
      at android.app.ActivityThread.main(ActivityThread.java:5001)
      at java.lang.reflect.Method.invokeNative(Native Method)
      at java.lang.reflect.Method.invoke(Method.java:515)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)
      at dalvik.system.NativeStart.main(Native Method)

下面就從方法getTransformation開始追溯,依次向上來揭開View動畫的執行過程。

Animation.getTransformation

public boolean getTransformation(long currentTime, Transformation outTransformation)

上面對Animation的分析中已經介紹過了,這里稍作重復。
getTransformation正是Animation的使用者獲取動畫結果的方法。調用者會傳遞一個表示當前動畫時間的currentTime參數,
它的返回值表示當前動畫執行后動畫是否還在運行,true表示還有更多動畫步驟。false意味著動畫已經結束了。

View.drawAnimation

調用方法getTransformation的方法是View.drawAnimation,它的大致內容如下:

/**
 * Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
 * case of an active Animation being run on the view.
 */
private boolean drawAnimation(ViewGroup parent, long drawingTime,
      Animation anim, boolean scalingRequired) {
      ...
      final Transformation t = parent.getChildTransformation();
      boolean more = a.getTransformation(drawingTime, t, 1f);
      if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
          if (parent.mInvalidationTransformation == null) {
              parent.mInvalidationTransformation = new Transformation();
          }
          invalidationTransform = parent.mInvalidationTransformation;
          a.getTransformation(drawingTime, invalidationTransform, 1f);
      } else {
          invalidationTransform = t;
      }    
      ...

      if (more) {
        ...
        parent.invalidate(left, top, right, bottom);
        ...
      }

      return more;
}

它是View類私有的一個完成動畫繪制的功能函數,參數anim就是View當前運行的Animation。方法里面執行了anim.getTransformation(drawingTime, t, 1f),t是從parent獲得的。如果getTransformation返回true,將繼續執行parent.invalidate語句,它使得View對應畫布的區域被標記為失效,這樣后續會執行一次onDraw來重繪。

上面提到過,開始動畫運行的兩個條件:

  1. 為動畫設置了startTime.
  2. view對象的parent需要變為invalidated.

anim是肯定已經被設置了startTime。從drawAnimation的代碼可以看到,當使用drawingTime調用getTransformation返回true時表示動畫還在運行,這時方法就執行確保上面條件2成立的邏輯。

上面的分析基本上給出了關于Animation.getTransformation不斷被執行的一點啟示。注意drawAnimation繼續返回getTransformation相同含義的返回值——動畫是否還在運行——給調用者,那么繼續看看更上面的方法如何使用它。

View.draw

方法原型:

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)

這個方法內部調用了上面的drawAnimation。然后返回drawAnimation相同的返回值。它是專門供
ViewGroup.drawChild方法調用的。
方法內部也會在drawAnimation返回true時執行一些“invalidate”邏輯。

調用流程小結

實際上到此,如果getTransformation返回true——動畫還在運行——那么View動畫執行的條件“make view's parent invalidated”已經成立了。從getTransformation到View.draw的調用已經可以解釋了Animation不斷被執行的原理了。

View類提供了invalidate系列方法用來通知ViewTree重繪。也就是觸發一次界面刷新。Animation的動畫計算需要startTime和當前動畫時間currentTime。

開始動畫的邏輯:
animation.setStartTime();
view.setAnimation(animation);
((View)(view.getParent())).invalidate();

繼續動畫的邏輯:
animation已經開始,startTime已經具備。
若currentTime和startTime比較得出動畫還未過期,則getTransformation返回true.
View.drawAnimation和View.draw中會對parentView執行“invalidate”邏輯。

ViewGroup.drawChild

此方法調用上面View.draw方法,原型如下:

/**
 * Draw one child of this View Group. This method is responsible for getting
 * the canvas in the right state. This includes clipping, translating so
 * that the child's scrolled origin is at 0, 0, and applying any animation
 * transformations.
 *
 * @param canvas The canvas on which to draw the child
 * @param child Who to draw
 * @param drawingTime The time at which draw is occurring
 * @return True if an invalidate() was issued
 */
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

注意其返回值的說明“True if an invalidate() was issued”,此方法直接返回的是View.onDraw的返回值,最終就是getTransformation的返回值。
因為執行了View.draw后,如果動畫還在運行,也就是這里返回true,那么已經對view對象的parent——也就是當前ViewGroup對象——執行了invalidate邏輯。所以這里返回true意味著一次invalidate()調用已經發起(was issued翻譯就是“已經發起...操作”)。

既然它繼續返回了這個一路來特別重視的“boolean值”,就再看它的返回值如何被使用。

ViewGroup.dispatchDraw

調用drawChild的是dispatchDraw方法,原型:

protected void dispatchDraw(Canvas canvas)

它不再向上返回任何值了,也就是Animation.getTransformation方法返回值的終點。
方法中,執行了drawChild獲得其返回值:more |= drawChild(canvas, child, drawingTime);,后續代碼只在more為false時對動畫結束做些處理。

Choreographer

可以說從Animation.getTransformation到View.draw方法調用的分析很清楚地說明了Animation動畫的開始執行和不斷執行直至結束的機制。完整的調用棧還有很多調用過程,但是,在“應用層面”分析Animation的原理的任務已經完成了,這里也不打算陷入“源碼泥沼”。

值得注意的一個方法是android.view.Choreographer.doFrame的調用,額外的對這個類留意一下——只因doFrame名字實在太叼,很明顯就是“執行一幀”的含義,和“顯示器刷新”的說法不謀而合。那就來瞅瞅它吧。
簡介如下:
協調動畫、輸入和繪制相關操作的時間。
可以想象,設備是最終決定繪制能力,計算能力的因素。Android提供的UI框架只能盡最大努力去協調不同計算操作的資源(GPU、CPU時間)的分配。
有興趣大家可以自行做更多研究,這里額外mark一下。

其它方法

再向上是一些Handler、Looper、ActivityThread.main、os.ZygoteInit.main這樣的類、方法,可能在Activity的onCreate或者一些點擊事件的調用棧可以看到最上層的調用方法幾乎都是這些。
可以“想象”它們都是有關UI事件的消息輪詢,對于一個打開了的界面,一方面UI線程需要不斷的繪制整個ViewTree的內容到屏幕,另一方面,還需要不斷檢測屏幕上的點擊等事件然后向下分發給需要處理它的View,Windows窗體以及游戲引擎等“UI框架”的原理大致都是如此,這里就沒法再做更多鉆研了。

Animation原理總結

這里將以上得到的線索進行整理。

Animation的設計

Animation封裝了動畫的參數——開始時間,持續時間,動畫的運行狀態的控制,以及動畫的操作。
它抽象了一個“View動畫”,它的使用者調用其getTransformation方法獲得運行中的動畫的計算結果。

Animation的運行

Animation的“動畫”的運行就是讓getTransformation不斷被執行——應該沒有只執行一次就結束的“動畫”吧?這涉及兩個任務:

  1. 觸發第一次執行getTransformation。
  2. 只要動畫還未結束,就繼續不斷執行getTransformation。

再回顧下上面得出的結論:
為了讓getTransformation得到執行:

  1. 必須為animation設置startTime,這很講道理,因為之后執被調用時會提供currentTime,那么動畫的狀態(是否過期)的參照startTime必須是賦值了的。
    因為invalidate通知重繪到下次重繪操作執行中間會有時差,所以設置animation的startTime為一個標記START_ON_FIRST_FRAME,這樣在第一次getTransformation實際被調用時,動畫的開始時間才是正確的“當前”毫秒數。

  2. 通知viewParent重繪
    要知道Animation動畫是針對View執行的,但是它往往可以改變View對象的繪制內容產生邊界的移動,也就是動畫會使得View的內容超出原有區域,所以,重繪操作是在view的ViewGroup中進行的。動畫如果超出ViewGroup的邊界,或者進入其它childView的區域,就會被裁剪。

現在看到上面的結論應該很容易理解了,getTransformation的執行最終是通過View所在的ViewGroup的繪制操作完成的,所以問題就轉為如何讓目標ViewGroup觸發重繪動作。
第一次觸發是“開始動畫”的邏輯,手動讓view的ViewGroup執行一次invalidate即可。動畫的后續持續執行就不用關心了,getTransformation的調用棧已經給出了原因:只有動畫還在運行,getTransformation返回true,那么調用它的上層方法(View.draw)就執行一次invalidate。

完整流程如下:

  1. 準備好animation對象:創建、設置參數,設置給View(被作為對應View的mCurrentAnimation字段)。
  2. view發起invalidate:
    2.1 invalidate向上通知ViewTree執行重繪,最終響應view回來的是一次ViewGroup的drawChild調用(在下一次繪制階段)。
    2.2 當前View所在的ViewGroup的drawChild方法調用其draw方法。
    2.3 draw中調用drawAnimation。
    2.4 drawAnimation調用mCurrentAnimation.getTransformation。
    2.5 getTransformation中執行Animation的applyTransformation。
    2.6 applyTransformation完成具體動畫邏輯,執行完成后調用棧開始返回。
    2.7 方法getTransformation返回值“動畫是否還在運行”給View.draw。
    2.8 draw中根據其返回值決定是否繼續調用invalidate觸發下一次動畫繪制。如果為true就繼續執行invalidate方法觸發下一次動畫繪制,否則動畫結束。

Animation的巧用

了解了以上有關View動畫的機制后,下面介紹一個有關Animation的巧妙用法。
如果你需要一個非常特殊的動畫,不是現有的Animation子類滿足要求,也不可以被組合。說的更直接點,你唯一需要的就是動畫的公共行為——開啟然后周期調用。
那么可以在重寫的applyTransformation方法中只利用它的interpolatedTime來獲得“動畫時間進度”,做你希望的一切。不用去關系Transformation outTransformation參數。

比如你可以在applyTransformation中不斷修改View的LayoutParameters來實際改變View的位置,就像屬性動畫那樣,雖然實際上完全就是屬性動畫的用武之地,但這里只是提供一個思路。

(本文使用Atom編寫)


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()