稀有猿诉

十年磨一剑,历炼出锋芒,说话千百句,不如码二行。

Android Animation Internal Secrets

前面的文章重点讲了如何使用安卓平台提供的能力来做好一个动画。为了更深入的理解,需要去了解一下动画框架的内部机理,这样能够帮助我们做出更优雅的动画实现。

View Animation的原理

View Animation源码解析

View animation的代码都是在android.view.animation包下面。

这里面主要有三个东西,下面来分别仔细说说

Animation

主要是抽象类Animation以及它的四大子类,也是View animation中的四大变幻对象–位移变幻TranslateAnimation,缩放变幻ScaleAnimation,旋转变幻RotateAnimation和渐变变幻AlphaAnimation

以及一些工具对象,如AnimationSetAnimationUtils

仔细看这些类的源码可以发现,其实它们不复杂,里面也没啥东西,主要是用于各种参数管理,相当于封装出来的工具和原料,具体内部的原理并不在这里。仔细看四大变幻的applyTransformation方法,可以发现这一坨把最接近『原理』的东西都放在了一个叫做Transformation的对象中去了。

Transformation

直译变幻,但文档中的定义是动画过程中某一时刻应该做的变幻,此为Transformation

这货的实现也不复杂,它也就是个中间商,只是一个存储从Animation传过来的参数 的中间变量,它里面有一个Alpha成员参数用以保存当前的渐变参数值,以及一个Matrix,Matrix可以保存当前的位移,旋转和缩放。Matrix应该不算太陌生,处理过Bitmap变幻的同学,对它应该会有了解,都是通过Matrix来设置参数的。

Interpolator

动画是随时间变化的一系列视觉变幻,因人眼视觉残留,连在一起就是动画,跟电影是一个道理。这里就有一个非常关键的参数就是时间。时间对于动画来说体现在两方面一是时长,就是整个动画持续 的时间,另外一个就是变幻变化的速率,也就是说动画播放速度的变化率。其实,这里变化的并不是时间,时间是永恒的以固定速度在流逝,对于动画来说,帧率是固定的,后面会谈到,动画的帧率是由时间驱动器驱动的,它是以固定的时间脉冲来回调渲染动画的每一帧。这里的时间变化其实是做动画的每一帧时用到的参数 的变化,它并不是线性的,假设动画一共有10帧,要把View向右移动100px,默认是线性的,匀速的,也即每一帧都向前移动10px,但如果使用加速插值器,那么可能就是一个变加速运动,第1帧可能在0px,第2帧在5px,第3帧25px,第4帧到36px,以此类推。

时间插值器,就是用来调整播放速度的,用以实现时间变化。

View Animation的渲染原理

从前面的讨论来看,动画的渲染跟那几个对象都没有关系,使用View animation的时候,只有两种方法可以让动画生效,一是调用View#startAnimation,另外一个是View#setAnimation,然后再Animation#start

假如没有把Animation塞给某一个具体的View对象,光是调用Animation#start,是不会有任何影响和效果的。这说明动画的渲染是在View对象draw时做的,没有与具体View对象建立关联的动画是没有任何效果的。所以动画的渲染主要还要看View本身的逻辑。

可以从View#setAnimation和View#startAnimation入手来看,这两个方法只是把外部传进来的Animation对象保存在了一个叫做mCurrentAnimation成员里面,其他的什么也没做。查询索引,关键的地方有两个,一个是View#applyLegacyAnimation方法,另外一个就是View#draw方法。

先来看View#applyLegacyAnimation方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   /**
     * 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 applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

        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;
        }

        // more codes
        return more;
    }

这个方法看着比较长,但它就做了三件事情:1)初始化动画;2)获取当前时刻的Transformation;3)如果动画还没有完(还有下一帧),那就得调用View的invalidate,得重绘。

再看使用此方法的地方,是在draw,需要注意是带有三个参数的那个draw,在前面的文章里面介绍过,这个draw方法是由ViewGroup#dispatchDraw中drawChild时调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
   /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        // More codes

        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
            if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
                // No longer animating: clear out old animation matrix
                mRenderNode.setAnimationMatrix(null);
                mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            if (!drawingWithRenderNode
                    && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                final Transformation t = parent.getChildTransformation();
                final boolean hasTransform = parent.getChildStaticTransformation(this, t);
                if (hasTransform) {
                    final int transformType = t.getTransformationType();
                    transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
                    concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
                }
            }
        }

        // more codes

        int restoreTo = -1;
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;

                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }

                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            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;
                    }
                }

                if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }
       }
        // more codes
        return more;
    }

这个方法更长,主要就看transformToApply这个变量就好了,这个变量是在调用了applyLegacyAnimation后被赋值的。之后,可以看到它其中的Matrix被作用于Canvas,而alpha值被用于setAlpha了。好了,这里就是动画的最核心的逻辑。前面说了Transformation对象就是包了一个Matrix和alpha,然后被用在了这里,Matrix作用于Canvas对象,以产生视觉变幻(位移,缩放和旋转),而渐变则是通过setAlpha实现的。

所以View Animation是View tree每次draw的时候去做的,用当前的Animation对象获取到Transformation,然后把Matrix和alpha应用到draw时的Canvas,这就产生了视觉变幻效果。因此,View animation只是放一遍电影,因为这一过程中变化 的只有Transformation对象,也即只有Matrix和alpha在变化,在View draw的时候应用一下就完了,它并没有对View的真实属性产生影响,仅是对渲染的结果Canvas产生影响。而每次View draw的时候,都是会重新生成一个Canvas对象,并且View的属性本身并没有变,所以新生成的Canvas对象并不会体现之前一次draw(也即上一帧)的变幻结果,它只是继续应用Transformation对象,假如动画结束了就没有了Transformation对象,那就没有Matrix和alpha可作用于Canvas,也就没有了动画效果,一切又恢复到了最初原始的样子。

Property Animation的原理

属性动画的实现主要是在android.animation里面,它有独立的一级包名,可以看出它在平台中的位置,是要高于View animation的。

Animator的源码解析

先从Animator对象看起,它是一个抽象类,只定义了关于动画的基本接口,如duration/start/end/cancel等,以及设置AnimatorListener以外,再无其他东西。

最为核心的对象是ValueAnimator,它是属性动画的核心,它主要有两部分,一是管理各种数值,前面的文章说过属性动画的核心原理就是在一定时间内,用一定速率把某个值变成另外一个值;另外一部分就涉及渲染原理,后面再详细说。

再有就是ObjectAnimator,它是ValueAnimator的子类,连同PropertyValuesHolder一起,针对某个对象的属性进行管理,主要涉及两方面,一个是属性值的管理,也即把对象的属性名字和其要设置的值都暂存起来,另外一部分就是通过反射来把要修改的值作用于目标对象。

Animator的时间驱动器

动画要让数值随时间而变化,当start了以后,最重要的事情 就是以一定的时间速率来刷新数值,也即是用一个时间驱动器来刷新每一帧。前面讨论了View animation,是在View tree渲染时去刷新动画的每一帧。

属性动画的核心在ValueAnimator里面,连同一个AnimatorHandler对象,一起实现了时间驱动。AnimatorHanndler是属性动画的时间驱动器,它从Choreographer中接收脉冲信号,然后再回调给所有的ValueAnimator,令其doAnimationFrame。它是一个单例,也就是说同一个进程里所有的属性动画用的是内一个时间驱动器,同一个AnimatorHandler。

注意:关于Choreographer的解释可以看另外的文章

当调用ValueAnimator#start时便会往AnimatorHandler对象添加一个回调,用以接收do frame的脉冲事件,然后从时间插值器mInterpolator中获取当前的时间速率,再调用animateValue进行数值的改变,其子类可以override此方法以实现属性的具体变化。这里还有一个变量mSelfPulsing用以控制是否使用AnimatorHandler,默认是true,也就是让ValueAnimator使用AnimatorHandler接收来自Choreographer的脉冲信号做动画。此外,也可以自己实现一个时间驱动器。

由此,便可以让在duration之内,渲染动画的每一帧。

Animator的渲染原理

ValueAnimator仅是让一个数值在一定时间内发生特定的变化,它没有实际的视觉效果。常常使用的是ObjectAnimator,并作用于View的属性以产生视觉效果,如前面文章中的例子。那么这个又是如何实现的呢?

ObjectAnimator是可能改变某个对象(内部称之为Target对象)的某个属性值,让其随时间变化,当应用到View对象时,比如translationY属性,ObjectAniamtor所做的也仅仅是让translationY的值随时间变化 而已,仅在animateValue时去调用View#setTranslationY把变化的数值传进去。是View自己在做重绘,View的setTranslationY方法中,有做invalidate以进行重绘。由此,便产生了视觉效果。

ViewPropertyAnimator是另一个常用的对象,但发现它并不是Animator的子类,是封装出来的专门针对View对象做属性动画的一个工具类,它本质上与ObjectAnimator一样,只不过做了一些集成与封装,可以同时方便的操作多个属性,另外它会把所有属性的值变更 过后统一调一次invalidate,效率上会略高一筹。ObjectAnimator一次只能操作一个属性,并且每个属性变化 时都会调一次invalidate。

它是把支持的属性都先放进一个map里面暂存起来,当调用startAnimation时,创建一个ValueAnimator,并设置一个AnimatorListener,在onAnimationUpdate时,把前面暂存的属性都设置到mView对象中去,然后调用一次invalidate让mView重绘。这里还需要注意,在设置属性这一块与ObjectAnimator也不一样,前面说了ObjectAniamtor是通过属性的settter来实现的,但View的属性的settter都会触发invalidate。所以,ViewPropertyAnimator为了避免每次设置属性时都触发invalidate,它是直接把属性塞给View的mRenderNode对象,然后在所有变化 的属性都设置完以后,再统一做一次重绘(invalidate)。

另外的区别就是,ViewPropertyAnimator仅支持一些特定的属性,而ObjectAnimator可以支持任意属性(只要有setter/getter,就可以)。

关于动画的常见问题

通过上面的论述,就搞清楚了动画原理了,下面来看一些比较有意思的问题。

动画是在主线程里做的么

动画主要是通过View对象来呈现视觉效果,View是在主线程中渲染的,所以动画也是在主线程里面完成的。这话呢,只对了一半,或者这么说是不够严谨的。

通过上面的讨论,View animation,都是在主线程中实现的,因为它的时间驱动器是View tree的渲染,也即在draw的时候,去计算当前的Transformation,然后应用到View的Canvas上面。这一切都是在主线程中完成的。

但对于属性动画,就不是这个样子,属性动画分两部分,一部分是让数值随时间变化 ,这个其实可以在任意线程中去做。通过上面的讨论,默认的情况下,确实也是在主线程中做的(从Choreographer得到时间脉冲,这是在主线程里面),但是留 有接口,可以改变的,虽然很少这样做,但确实是可行的,并且数值随时间变化,这个事情也是可以在任意线程中完成的。另外一部分,就是让变化 的数值对目标对象生效,这个要看具体的对象了,如果View,肯定 还是要在主线程里搞。

动画的帧率(FPS)是多少

从上面的讨论来看,无论是View animation还是属性动画,时间脉冲都是Choreographer,并且对View来说视觉要生效是通过重绘来做的,所以最高帧率都会是60FPS。

所以,其实动画的帧率是固定的,也就是说其doAnimationFrame是固定频率在回调。

这里要与动画的时间插值器区别开来,动画的真实帧率是固定的,时间插值器的作用是让动画的变化变成非线性的。比如说某个属性x从0变到100,ValueAnimator的doAnimationFrame以及animateValue会是以固定的频率,从Choreographer每隔16ms接收一次脉冲,就会调用一次animateValue,时间插值器的作用,能让x值的变化是非线性的:

时间脉冲:0 1 2 3 4 5 6 7 8 9 10 线性变化:0 10 20 30 40 50 60 70 80 90 100 加速减速:0 13 25 37 57 71 79 85 89 95 100

时间插值器并没有让动画的帧率发生变化 ,而是让动画的结果非线性变化。

动画过程中如何处理MotionEvent事件

没有任何影响,view animation是发生在draw的时候,而属性动画是设置属性后再re-draw。从逻辑 上来讲动画与事件不冲突,两者之间没有任何影响。

不过呢,View animation是对Canvas做变幻,View对象仍在原来的位置,原来的状态,所以点击动画过程中的View可能会没有效果,特别是对于有位移的时候。但属性动画就没有问题,View就是真实的在移动。

但对于业务逻辑来说,通常动画都用于某个View的入场和出场,所以入场动画做完之前,以及出场动画开始之后,不响应点击事件要好一些,当然,这个就要靠开发者自己去实现了。

动画可以取消么

当然可以,都有cancel接口可以调用,但具体影响不太一样。

对于View animation,Animation#cancel是会调用onAnimationEnd的,因为它的回调接口没有专门用于cancel的。

但属性动画的回调接口要丰富一些,它有cancel,所以是会回调onAimationCancel的,但不会回调onAnimationEnd。

动画需要注意的事项

一定要实现onAnimationCancel,以及onAnimationEnd,如果有涉及状态变更,或者关联其他动画时。要知道动画除了常规结束还会有被cancel掉的可能。

另外,就是对于属性动画,取消有两种方式,一是直接调用Animator#cancel另外一种是调用Animator#end,两个方法在处理最后的状态时略有差异。end方法会把属性的最终状态设置给属性,然后回调onAnimationEnd,但cancel就直接终止动画了,属性当前啥状态那就啥状态,然后回调onAnimationCancel。其实,大多数情况下,end更为合理,但end可能会造成视觉上的跳跃,属性的状态会突然变化。

再有就是,如果对于View,有多个属性同时做动画时,用ViewPropertyAnimator更好一些。语法上面也更简洁,性能上也略优一些。

Comments