Android属性动画工作原理
什么是属性动画
更改一个对象的属性值时,值的变化呈现动画效果。如一个Drawable的alpha值变化,或者一个Drawable在view上位置的变化。
属性动画 vs 视图动画
当属性动画作用于view的属性时,它呈现的效果和视图动画相视,但它们还是有区别的:
属性动画 vs 视图动画.png
主要就是视图动画仅针对View、Surface等视图,而属性动画不只针对视图;视图动画改变的仅仅是视觉效果,而属性动画改变的是值本身,这也就是对一个view做如位移等动画时,视图动画结束后view的点击区域还在原区域,而属性动画view的点击区域就在view呈现的位置。从包名上看也很明显,属性动画的包为android.animation,而视图动画为android.view.animation。
动画分类.png
Animator
Animator是属性动画的基类,给动画提供开始、结束、动画状态回调等基础操作。
Animator.png
它主要做的工作就是通过以下步骤来完成动画效果:
- 调用者提供需要做动画的属性的初始值startValue和结束值endValue,及动画时长;
- 每隔固定的时间间隔返回这个属性的一个值给调用者;
- 调用者把这个值设给动画对象的属性;
如果在每个时间间隔到达时都通过同一个公式根据当前动画的完成时长计算属性值的话,我们获得的动画效果肯定为线性的。
如:我们需要在40ms内把一个view从x=0的位置移动到x=40的位置,公式为f(v) = startValue + t * (endValue - startValue),其中 t = elapsedTime / totalTime线性效果如下:
google官方:animation-linear.png
但一般情况下,我们需要这个动画有一个变化节奏,如先慢后快再慢:
google官方:animation-nonlinear.png
这个时候我们需要每个时间间隔的计算公式返回的值是非线性的,这样就需要这个公式有个变量,最简单的就是根据当前时间完成百分比来控制这个变量的值,如上面的非线性方程式可以为f(v) = startValue + f’(t) * (endValue - startValue),其中f'(t) = Math.cos(t+ 1) * Math.PI / 2.0f + 0.5f,t = elapsedTime / totalTime。在Animator模型里f(v)的计算封装在TypeEvaluator类里,而f'(t)的计算封装在TimeInpterpolator里,Animator只需在每个固定的时间间隔时传入t(当前时间点,动画时间的完成百分比)即可。
TimeInterpolator
如上面的f'(t) = Math.cos(t+ 1) * Math.PI / 2.0f + 0.5f,t = elapsedTime / totalTime。TimeInterpolator提供了一个公式或匹配方式来获取当前动画完成时间百分比的插值,确保在计算属性值时有一个变化率参数。它的输入为当前动画时长的完成百分比,输出为插值。
Android提供的插值器有下面几种,当然你也可以自己定义:
时间插值器:TimeInterpolator.png
TypeEvaluator
如上面的f(v) = startValue + f’(t) * (endValue - startValue)。TypeEvaluator提供公式根据当前TimeInterpolator提供的带有变化率的动画时长完成百分比插值来计算属性值。它的输入为带变化率的动画完成时长百分比,输出为属性值。
Android提供的TypeEvaluator有下面几种(可自定义):
类型评估器:TypeEvaluator.png
TimeInterpolator 和 TypeEvalulator 的关系
TimeInterpolator & TypeEvaluator<T>.png如何在固定的时间间隔开始属性值计算?
上面我们提到的是每隔固定的时间间隔来获取属性值,那么怎么做到呢,或者可不可以根据有变化率的时间间隔来获取值呢(这样不需要TimeInterpolator)?
不管是drawable还是view,在动画更改某件属性后,都需要更新显示,所以我们最有效的方法是让其和view的刷新在同一个脉冲里进行。这样我们很容易就想到根据显示子系统的VSYNC脉冲来更新动画,实际上android的动画系统也是这么做的。Animator通过AnimationHandler向Choreographer注册CALLBACK_ANIMATION来获取固定脉冲即固定时间间隔,在收到脉冲时更新动画。
Choreographer从显示子系统接收定时脉冲VSYN:
Choreographer.png
AnimationHandler向Choreographer注册定时脉冲回调:
AnimationHandler.png
因此我们总结下我们的思路:
思路.png
流程总结下来就是:
Animator流程.png
ValueAnimator
ValueAnimator就是实现Animator的一个类,它在动画时根据当前脉冲时间计算动画属性值,然后返回给动画调用方:
ValueAnimator实现Animator思路.png
其中最重要的就是根据当前脉冲时间计算动画属性值:
根据当前帧时间计算动属性的动画值.png
其中TypeConverter是在TypeEvaluator计算出来的值和动画对象要设的属性值类型不一致时使用,如动画对象的属性值类型为PointF,而TypeEvaluator计算的值类型为float:
TypeConverter<T, V>.png
另外它实现了重复动画、反向动画、指定延时后开始动画的功能:
属性动画可以定义的特征.png
具体的实现原理如下:
ValueAnimator.png
其中我们用到了PropertyValueHolder,它封装了动画对象的属性名和在动画过程中这个属性的一组值。
我们首先看下Property<T, V>和Keyframe是指什么,Property<T, V>封装了 属性名/属性值,而Keyframe封装了 时间点/属性值。
PropertyValuesHolder vs Property vs Keyframe vs Keyframes.png
再看看Property的实现:
Property<T, V>.png
Keyframe的实现:
Keyframe & Keyframes.png
PropertyValuesHolder的实现:
PropertyValuesHolder.png
其实很简单,就是保存了propertyName和Keyframes,要注意的点就是它获取保存了这个属性名的mSetter和mGetter反射方法,如我们对象View的属性名propertyName = "alpha",它获取保存了View#setAlpha(float alpha)和View#getAlpha()的反射Method,这在ObjectAnimator中会用到。
ObjectAnimator
根据上面ValueAnimator的内容,我们在计算出属性值后要返回给动画调用方让它自己来重新设置对象的属性值。那我们能不能直接帮忙把属性值设给动画对象?
答案当然是肯定,上面PropertyValuesHolder的实现原理时我们看到它获取了动画属性的setXXX()反射Method,我们完全可以在计算结束后用这个反射方法来给动画对象更新动画属性值,只需给Animator多传入一个动画对象。
ObjectAnimator vs ValueAnimator
ValueAnimator vs ObjectAnimator.png原理
ObjectAnimator.png由上面知道,ObjectAnimator的核心思想在于用反射方法来更新动画对象的属性值,这要求我们的动画属性在其类里必需有setXXX()的方法,如果这个set方法有参数的话还必需有getXXX()方法。
ViewPropertyAnimator
如上面ValueAnimator和ObjectAnimator在给View的多个属性做动画时,每个属性值更改时调用一次view.setXXX()方法,而每个setXXX()方法的实现里都调用了invalidate()一次,如果同时给多个View的属性做动画的话,显然性能不高,那么我们可不可以更新多个View的属性值时只调用一次invalidate()呢?ViewPropertyAnimator就是通过直接调用RenderNode.setXXX()来更改属性值,再统一调用一次invalidate()来使原布局失效。
ViewPropertyAnimator vs ObjectAnimator
ObjectAnimator vs ViewPropertyAnimator.png通过上图可以看到,ViewPropertAnimator在一次给多个View的属性做动画时拥有更高优的性能,同时它的调用方式也简单多了。但它的动画对象仅仅是View。
其流程:
ViewPropertyAnimator流程.png
原理
ViewPropertyAnimator的实现和ValueAnimator不一样的地方在于:
- 不需要直接调用start(),会根据View的Choreographer的脉冲回调自动触发。
-
并没有通过ValueEvaluator计算值,而是直接写死了计算值的公式。
ViewPropertyAnimator原理.png
作用对象
那么ViewPropertyAnimator是不是实现了View中所有属性的动画呢?并不是。其实现的动画属性如下:
ViewPropertyAnimator中支持的View的属性.png
TimeAnimator
如果我仅仅只想知道动画时长的消耗情况,不需要计算任何的属性值,有没有简便的方法呢?TimeAnimator就提供了这个功能。
TimeAnimator.png
它提供一个回调函数返回:
- 上一帧到这一帧之间的时间增量;
- 动画启动以来的总时间。
AnimatorSet
AnimatorSet可以按指定顺序(如,一起、按顺序、延时)播放一组Animator对象。
可以通过
- AnimatorSet#playTogether(Animator[])来指定一组Animator同时播放;
- AnimatorSet#playSequentially(Animator[])来指定一组Animator依次播放;
- Animator#play(Animator)和Builder类中的方法构建一组Animator的播放顺序。
如果AnimatorSet中的某些Animator的播放顺序构成了死循环,如a1->a2->a3->a1,将表示这些动画将永远播放下去。
流程
其流程很简单如下:
AnimatorSet流程图.png
原理
其原理简单来说就是:
- 根据动画是同时播放、还是先后播放构建成一个关系图,在A动画之前播放的动画是其parent,和A动画一起播放的是其sibling,在A动画之后播放的是其child。
- 定义一个时长为0的root动画,把没有parent的动画设为其child。
- 通过parent的结束时间计算child的开始时间,再根据这个开始时间和child本身的动画总时长计算child的结束时间。
-
把每个动画和其开始时间、结束时间封装成event,然后按照启动的先后顺序加入到一个队列中,每个时间脉冲到来时,把这个脉冲之前需要执行的event拿出来执行。
如下:
AnimatorSet中Animator的关系图.png
总的实现原理:
AnimatorSet.png
release版本的ObjectAnimator启用失败?
我们是否会碰到一个ObjectAnimation在debug版本中运行的好好的,但是release版本就不行?
由上面ObjectAnimator的实现原理可以看到,我们在初始化时如果没有传入某个属性的初始值,它会用反射方法去拿这个属性的当前值为其初始值;在属性值计算完成后,也会用反射方法去把值设为对象的属性值。当release版本把这个动画对象的类混淆时,反射方法找不到,于是动画无法正常运行。
参考
Android官方:属性动画
Android官方:ObjectAnimator
Android官方推荐博文:ViewPropertyAnimator
Android官方:View
material: elevation
material: elevation & shadow
Android官方:shadow
Android官方:animation-resource
原创文章,欢迎转载,但请注明出处