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

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