虾扯蛋:Android View动画 Animation不完全解析

本文结合一些周知的概念和源码片段,对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的位置,就像属性动画那样,虽然实际上完全就是属性动画的用武之地,但这里只是提供一个思路。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏偏前端工程师的驿站

WebComponent魔法堂:深究Custom Element 之 标准构建

前言  通过《WebComponent魔法堂:深究Custom Element 之 面向痛点编程》,我们明白到其实Custom Element并不是什么新东西,...

208100
来自专栏天天

框架设计续集(二)

9430
来自专栏hightopo

原 基于 HTML5 WebGL 的 3D

23940
来自专栏web前端教室

前端面试大坑实录

找工作嘛,要面试;公司招人嘛,也要面试。你面试是答题,公司招人是出题;双方都是陌生的,怎么凑成一对呢?就要靠面试题来牵线。在这个过程中呢,自然免不了要通过面试题...

9820
来自专栏懒人开发

CoordinatorLayout使用(二):Behavior流程 和 事件流

上一篇,我们大体理解了 Behavior简单理解 具体代码可以见 https://github.com/2954722256/use_little_demo ...

18960
来自专栏進无尽的文章

实践-小细节 Ⅰ

     开发中总有一些细枝末节的东西是容易出错的地方,搜集总结下,避免再次掉入坑中。

9020
来自专栏贾鹏辉的技术专栏@CrazyCodeBoy

React Native之React速学教程(下)

React Native之React速学教程(下) 本文出自《React Native学习笔记》系列文章。 React Native是基于React的,在开发R...

32450
来自专栏Android先生

Android资源那些事儿

--以下:正文部分-- Android的设计哲学为:设计与表现分离。 这样有利于程序的解耦。所以我们才可以在XML文件中定义各种资源类型,并在其他的xml文件或...

12620
来自专栏AndroidTv

View.animate()动画ViewPropertyAnimator原理解析

这次想来讲讲 View.animate(),这是一种超好用的动画实现方式,用这种方式来实现常用的动画效果非常方便,但在某些场景下会有一个坑,所以这次就来梳理一下...

42750
来自专栏JackieZheng

可视化(番外篇)——SWT总结

  本篇主要介绍如何在SWT下构建一个应用,如何安装SWT Designer并破解已进行SWT的可视化编程,Display以及Shell为何物、有何用,SWT中...

205100

扫码关注云+社区

领取腾讯云代金券