稀有猿诉

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

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

        if (more) {
            if (!a.willChangeBounds()) {
                if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                        ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                    parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
                } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                    // The child need to draw an animation, potentially offscreen, so
                    // make sure we do not cancel invalidate requests
                    parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    parent.invalidate(mLeft, mTop, mRight, mBottom);
                }
            } else {
                if (parent.mInvalidateRegion == null) {
                    parent.mInvalidateRegion = new RectF();
                }
                final RectF region = parent.mInvalidateRegion;
                a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                        invalidationTransform);

                // The child need to draw an animation, potentially offscreen, so
                // make sure we do not cancel invalidate requests
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                final int left = mLeft + (int) region.left;
                final int top = mTop + (int) region.top;
                parent.invalidate(left, top, left + (int) (region.width() + .5f),
                        top + (int) (region.height() + .5f));
            }
        }
        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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
   /**
     * 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) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
         * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
         * HW accelerated, it can't handle drawing RenderNodes.
         */
        boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;

        boolean more = false;
        final boolean childHasIdentityMatrix = hasIdentityMatrix();
        final int parentFlags = parent.mGroupFlags;

        if ((parentFlags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) != 0) {
            parent.getChildTransformation().clear();
            parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }

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

        concatMatrix |= !childHasIdentityMatrix;

        // Sets the flag as early as possible to allow draw() implementations
        // to call invalidate() successfully when doing animations
        mPrivateFlags |= PFLAG_DRAWN;

        if (!concatMatrix &&
                (parentFlags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
                        ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
                canvas.quickReject(mLeft, mTop, mRight, mBottom) &&
                (mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
            mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
            return more;
        }
        mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;

        if (hardwareAcceleratedCanvas) {
            // Clear INVALIDATED flag to allow invalidation to occur during rendering, but
            // retain the flag's value temporarily in the mRecreateDisplayList flag
            mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
            mPrivateFlags &= ~PFLAG_INVALIDATED;
        }

        RenderNode renderNode = null;
        Bitmap cache = null;
        int layerType = getLayerType(); // TODO: signify cache state with just 'cache' local
        if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
             if (layerType != LAYER_TYPE_NONE) {
                 // If not drawing with RenderNode, treat HW layers as SW
                 layerType = LAYER_TYPE_SOFTWARE;
                 buildDrawingCache(true);
            }
            cache = getDrawingCache(true);
        }

        if (drawingWithRenderNode) {
            // Delay getting the display list until animation-driven alpha values are
            // set up and possibly passed on to the view
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.hasDisplayList()) {
                // Uncommon, but possible. If a view is removed from the hierarchy during the call
                // to getDisplayList(), the display list will be marked invalid and we should not
                // try to use it again.
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }

        int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }

        final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
        final boolean offsetForScroll = cache == null && !drawingWithRenderNode;

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

            // Deal with alpha if it is or used to be <1
            if (alpha < 1 || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
                if (alpha < 1) {
                    mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                } else {
                    mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
                }
                parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                if (!drawingWithDrawingCache) {
                    final int multipliedAlpha = (int) (255 * alpha);
                    if (!onSetAlpha(multipliedAlpha)) {
                        if (drawingWithRenderNode) {
                            renderNode.setAlpha(alpha * getAlpha() * getTransitionAlpha());
                        } else if (layerType == LAYER_TYPE_NONE) {
                            canvas.saveLayerAlpha(sx, sy, sx + getWidth(), sy + getHeight(),
                                    multipliedAlpha);
                        }
                    } else {
                        // Alpha is handled by the child directly, clobber the layer's alpha
                        mPrivateFlags |= PFLAG_ALPHA_SET;
                    }
                }
            }
        } else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
            onSetAlpha(255);
            mPrivateFlags &= ~PFLAG_ALPHA_SET;
        }

        if (!drawingWithRenderNode) {
            // apply clips directly, since RenderNode won't do it for this draw
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                if (offsetForScroll) {
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    if (!scalingRequired || cache == null) {
                        canvas.clipRect(0, 0, getWidth(), getHeight());
                    } else {
                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                    }
                }
            }

            if (mClipBounds != null) {
                // clip bounds ignore scroll
                canvas.clipRect(mClipBounds);
            }
        }

        if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((RecordingCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
                // no layer paint, use temporary paint to draw bitmap
                Paint cachePaint = parent.mCachePaint;
                if (cachePaint == null) {
                    cachePaint = new Paint();
                    cachePaint.setDither(false);
                    parent.mCachePaint = cachePaint;
                }
                cachePaint.setAlpha((int) (alpha * 255));
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                // use layer paint to draw the bitmap, merging the two alphas, but also restore
                int layerPaintAlpha = mLayerPaint.getAlpha();
                if (alpha < 1) {
                    mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
                }
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
                if (alpha < 1) {
                    mLayerPaint.setAlpha(layerPaintAlpha);
                }
            }
        }

        if (restoreTo >= 0) {
            canvas.restoreToCount(restoreTo);
        }

        if (a != null && !more) {
            if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
                onSetAlpha(255);
            }
            parent.finishAnimatingView(this, a);
        }

        if (more && hardwareAcceleratedCanvas) {
            if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
                // alpha animations should cause the child to recreate its display list
                invalidate(true);
            }
        }

        mRecreateDisplayList = false;

        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