乐趣区

关于android:Android动画内幕揭秘

原文链接 Android Animation Internal Secrets

后面的文章重点讲了如何应用安卓平台提供的能力来做好一个动画。为了更深刻的了解,须要去理解一下动画框架的外部机理,这样可能帮忙咱们做出更优雅的动画实现。

View Animation 的原理

View Animation 源码解析

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

这外面次要有三个货色,上面来别离认真说说

Animation

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

以及一些工具对象,如 AnimationSet 和 AnimationUtils。

认真看这些类的源码能够发现,其实它们不简单,外面也没啥货色,次要是用于各种参数治理,相当于封装进去的工具和原料,具体外部的原理并不在这里。认真看四大变幻的 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 办法:

   /**
     * 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 时调用的:

   /**
     * 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 更好一些。语法下面也更简洁,性能上也略优一些。

原创不易,打赏 点赞 在看 珍藏 分享 总要有一个吧

退出移动版