本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布node
此次想来说讲 View.animate(),这是一种超好用的动画实现方式,用这种方式来实现经常使用的动画效果很是方便,但在某些场景下会有一个坑,因此此次就来梳理一下它的原理。android
首先,先来看一段代码:微信
mView.animate().sacleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
能够有些人还没接触过这个,但并不妨碍首次理解上述代码。单从方法名上来看,上述代码就是一个实现了持续 1s 的放大 & 透明度结合的动画,是否是发现使用起来特别简单,一行代码就搞定。数据结构
固然,上述的动画效果也能够经过 ValueAnimator 或 ObjectAnimator 来实现,只是可能无法像上述同样一行代码就搞定。若是用 Animation 来实现,那么须要的代码就更多了。ide
因此,咱们的问题就来了:函数
Q1:动画基本能够分为 Animator 和 Animation 两大类,而 View.animate() 返回的是一个 ViewPropertyAnimator 类型的对象,这个类并无继承自任何类,那么它实现动画的原理又是什么呢?单从命名上看好像是经过 Animator 实现,那么真的是这样么?post
Q2:开头说了,使用这种方式实现的动画在某些场景下会有一个坑,这个坑又是什么,是在什么场景下的呢?学习
好了,下面就开始来跟着源码一块儿学习吧:动画
ps:本篇阅读的源码版本都是 android-25,版本不同,源码可能会有些许差异,大伙本身过的时候注意一下。ui
那么,源码阅读的着手点就跟以前几篇分析动画的同样,从 start()
开始一步步跟踪下去就好了。
//ViewPropertyAnimator#start() public void start() { mView.removeCallbacks(mAnimationStarter); startAnimation(); }
代码不多就两行,第二行是调用了一个方法,看方法名能够猜想应该是去处理动画开始的工做,那么在动画开始前还移除了一个回调,但要搞清楚第一行的代码是干吗用的,咱们得先知道两个变量的含义,首先是第一个 mView:
//ViewPropertyAnimator构造函数 ViewPropertyAnimator(View view) { mView = view; view.ensureTransformationInfo(); }
mView 是一个成员变量,在构造函数中被赋值,还记得吧,要用这种方式实现动画时,都得先调用 View.animate() 来创造一个 ViewPropertyAnimator 对象,因此去 View 的 animate()
方法里瞧瞧:
//View#animate() public ViewPropertyAnimator animate() { if (mAnimator == null) { mAnimator = new ViewPropertyAnimator(this); } return mAnimator; }
这个方法里会去建立一个 ViewPropertyAnimator 对象,并将 View 自身 this 做为参数传递进去,也就是说,在 ViewPropertyAnimator 里的 mView 变量其实指向的就是要进行动画的那个 View。
知道了 mView 其实就是须要进行动画的那个 View 后,接下去来看看另外一个变量 mAnimationStarter 是什么了:
//ViewPropertyAnimator.mAnimationnStarter private Runnable mAnimationStarter = new Runnable() { @Override public void run() { startAnimation(); } };
这个 Runnable 就是一个启动动画的工做,emmm,这样就有点奇怪了,咱们再回过头来看看 start()
方法:
//ViewPropertyAnimator#start() public void start() { mView.removeCallbacks(mAnimationStarter); startAnimation(); }
为何明明方法的第二行就会去执行 startAnimation()
了,第一行却又要去取消一个执行 startAnimation()
的 Runnable 呢?
只能说明,在咱们调用 start()
以前,ViewPropertyAnimator 内部就已经预先安排了一个会执行 startAnimation()
的 Runnable 进入待执行状态,因此在调用了 start()
以后先去取消这个 Runnable 才会有意义。
那么,又是哪里会去触发安排一个 Runnable 呢?
回头再看看咱们使用这种方式来实现动画效果是怎么用的:
mView.animate().sacleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
首先,经过 View.animate()
先建立一个 ViewPropertyAnimator 对象,中间设置了一系列动画行为,最后才调用了 start()
。那么,有机会去触发安排一个待执行的 Runnable 操做也只能发生在中间的这些方法里了,那么咱们选择一个跟进去看看,scaleX()
:
//ViewPropertyAnimator#scaleX() public ViewPropertyAnimator scaleX(float value) { animateProperty(SCALE_X, value); return this; }
继续跟进去看看:
//ViewPropertyAnimator#animateProperty() private void animateProperty(int constantName, float toValue) { float fromValue = getValue(constantName); float deltaValue = toValue - fromValue; animatePropertyBy(constantName, fromValue, deltaValue); }
至于各个参数是什么意思,咱们后面再来分析,目前咱们是想验证是否是这些封装好的动画接口内部会去触发一个待执行的 Runnable 操做,因此优先继续跟踪下去:
//ViewPropertyAnimator#animatePropertyBy() private void animatePropertyBy(int constantName, float startValue, float byValue){ ... mView.removeCallbacks(mAnimationStarter); mView.postOnAnimation(mAnimationStarter); }
终于找到了,并且不只仅是 scaleX()
方法,其余封装好的动画接口如 scaleY()
,alpha()
,translationX()
等等全部这一系列的方法内部最终都会走到 animatePropertyBy()
里去。而在这个方法最后都会先将待执行的 Runnable 先移除掉,再从新 post。
要理解这么作的用意,得先明白 View 的这两个方法:removeCallbacks()
,postOnAnimation()
是干吗用的。这里我就不跟下去了,直接给大伙说下结论:
经过 postOnAnimation()
传进去的 Runnable 并不会被立刻执行,而是要等到下一个屏幕刷新信号来的时候才会被取出来执行。
那么,将这些串起来,也就是说,仅仅只是 View.animate().scaleX()
这样使用时,就算不主动调用 start()
,其实内部也会自动安排一个 Runnable,最迟在下一个屏幕刷新信号来的时候,就会自动去调用 startAnimation()
来启动动画。
但若是主动调用了 start()
,内部就须要先将安排好的 Runnable 操做取消掉,而后直接调用 startAnimation()
来启动动画。
那么,接下去就来看看是如何启动动画的,startAnimation()
:
//ViewPropertyAnimator#startAnimation() private void startAnimation() { //1. 这里我还没搞懂,也不清楚什么场景下会知足这里的条件,直接 return。因此,本篇接下去的分析都是基于假设会直接跳过这里,后面若是搞懂了再来填坑。 if (mRTBackend != null && mRTBackend.startAnimation(this)) { return; } ... //2. 建立一个 0.0-1.0 变化的 ValueAnimator ValueAnimator animator = ValueAnimator.ofFloat(1.0f); //3. 将当前 mPengingAnimations 里保存的一系列动画全都取出来,做为同一组一块儿执行一块儿结束的动画 ArrayList<NameValuesHolder> nameValueList = (ArrayList<NameValuesHolder>) mPendingAnimations.clone(); ... //4. 建立一个新的 PropertyBundle 来保存这一组动画,以ValueAnimator做为key来区分 mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList)); //5. 提供动画开始前,结束后的操做回调 if (mPendingSetupAction != null) { mAnimatorSetupMap.put(animator, mPendingSetupAction); mPendingSetupAction = null; } ... //6. 对ValueAnimator进行 Listener、StartDelay、Duration、Interpolator 的设置 animator.addUpdateListener(mAnimatorEventListener); animator.addListener(mAnimatorEventListener); ... //7. 启用ValueAnimator.start() animator.start(); }
上述代码能够先不用细看,咱们稍后再来一块一块慢慢过,我已经将整个方法里作的事大概划分红了 7 件,首先有一点须要提一下,方法内实际上是经过 ValueAnimator 来实现的。
上一篇博客属性动画 ValueAnimator 运行原理全解析中,咱们已经将 ValueAnimator 的运行原理分析完了,感兴趣的能够回去看看,这里大概提几点结论:
ValueAnimator 内部其实并无进行任何 ui 操做,它只是提供了一种机制,能够根据设定的几个数值,如 0-100,内部本身在每一帧内,根据当前时间,第一帧的时间,持续时长,以及插值器规则,估值器规则来计算出在当前帧内动画的进度并映射到设定的数值区间,如 0-100 区间内映射以后的数值应该是多少。
既然 ValueAnimator 并无进行任何 ui 操做,那么要用它来实现动画效果,只能本身在 ValueAnimator 提供的每一帧的回调里(AnimatorUpdateListener),本身取得 ValueAnimator 计算出的数值,来自行应用到须要进行动画效果的那个 View 上。
想一想本身使用 ValueAnimator 的时候是否是这样,咱们并无将 View 做为参数传递给 ValueAnimator,因此它内部也就没有持有任何 View 的引用,天然作不了任何 ui 操做。
因此看看 startAnimation()
方法里的,我标出来的第 二、六、7点:
//ViewPropertyAnimator#startAnimation() private void startAnimation() { ... //2. 建立一个 0.0-1.0 变化的 ValueAnimator ValueAnimator animator = ValueAnimator.ofFloat(1.0f); ... //6. 对ValueAnimator进行 Listener、StartDelay、Duration、Interpolator 的设置 animator.addUpdateListener(mAnimatorEventListener); animator.addListener(mAnimatorEventListener); ... //7. 启用ValueAnimator.start() animator.start(); }
因此,ViewPropertyAnimator 实际上是经过 ValueAnimator.ofFloat(1.0f)
,也就是借助 ValueAnimator 的机制,来计算每一帧动画进度在 0-1 内对应的数值。而后在它的每一帧的回调里再去进行 view 的 ui 操做来达到动画效果,那么 ui 操做也就是在 mAnimatorEventListener 里作的事了,跟进去看看:
//ViewPropertyAnimator.mAnimatorEventListener private AnimatorEventListener mAnimatorEventListener = new AnimatorEventListener(); private class AnimatorEventListener implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { ... @Override public void onAnimationUpdate(ValueAnimator animation) { ... //1. 取出 ValueAnimator 计算出的当前帧的动画进度 float fraction = animation.getAnimatedFraction(); //2. 根据取得的动画进度,进行一系列view的ui操做,来达到动画效果 ... } }
省略了绝大部分代码,等会会再来慢慢过,这样省略后比较容易梳理出整个流程,优先将流程梳理清楚,再来分析每一个步骤具体干的活。
因此,能够看到,ViewPropertyAnimator 确实是在 ValueAnimator 的每一帧的回调中,取得 VauleAnimator 机制计算出来的动画进度值,而后自行进行 ui 操做来达到动画效果。
那么,到这里,整个流程就已经梳理出来了,咱们先来梳理一下目前的信息:
经过 View.animate().scaleX(1.2f).start()
实现的动画,若是外部没有手动调用 start()
方法,那么 ViewPropertyAnimator 内部最迟会在下一帧的时候自动调用 startAnimation()
来启动动画。
ViewPropertyAnimator 实现下一帧内自动启动动画是经过 View.postOnAnimation()
实现,View 的这个方法会将传递进来的 Runnable 等到下一帧的时候再去执行。
若是外部手动调用了 start()
,那么内部会先将第 2 步中安排的自动启动动画的 Runnable 取消掉,而后直接调用 startAnimation()
启动动画。
startAnimation()
启动动画,其实是借助 ValueAnimator 的机制,在 onAnimationUpdate()
里取得每一帧内的动画进度时,再自行进行对应的 ui 操做来达到动画效果。
ValueAnimator 只是会根据当前时间,动画第一帧时间,持续时长,插值器规则,估值器规则等来计算每一帧内的当前动画进度值,而后根据关键帧机制来映射到设定的范围内的数值,最后经过每一帧的进度回调,供外部使用,它自己并无任何 ui 操做(详情可看上一篇博客)。
好了,流程上已经梳理清理了,接下去就是细节问题了,ViewPropertyAnimator 取得了每一帧对应的动画进度时又是如何进行的 ui 操做的呢?View.animate()
后面是支持一系列的动画操做,如 scaleX()
,alpha()
等一块儿执行的,那么内部又是如何区分,维护的呢?
咱们仍是按照流程来一步步详细的分析,View.animate() 方式实现的动画,流程上是设置动画行为--启动动画--每一帧进度回调中进行ui操做。因此,下面就先看看第一个步骤,跟着 scaleX()
进去看看:
//ViewPropertyAnimator#scaleX() public ViewPropertyAnimator scaleX(float value) { //1. 第一个参数用于区分不一样的动画,第二个参数设定动画最后一帧的值 animateProperty(SCALE_X, value); return this; } //ViewPropertyAnimator#animateProperty() private void animateProperty(int constantName, float toValue) { //2. 第一步先取得该种动画行为下的默认第一帧值,最后一帧值就是参数传递进来 float fromValue = getValue(constantName); //3. 计算出动画的变化数值 float deltaValue = toValue - fromValue; animatePropertyBy(constantName, fromValue, deltaValue); } //ViewPropertyAnimator#getValue() private float getValue(int propertyConstant) { final RenderNode node = mView.mRenderNode; switch (propertyConstant) { ... //4. 直接经过 getScaleX() 取得当前 view 的默认属性值 case SCALE_X: return node.getScaleX(); ... } return 0; }
上述代码做用,其实也就只是取得对应动画行为下的第一帧的属性值,而后根据设定的最后一帧属性值来计算出动画变化的数值,最终做为参数传递给 animatePropertyBy()
,因此最关键的任务确定在这个方法里,但要捋清楚这个方法里的代码前,还须要先了解一些变量以及内部类的含义:
ViewPropertyAnimator 内部有两个数据结构类 NameValuesHolder 和 PropertyBundle,都是用于存储各类动画信息的,除此以外,还有一系列成员变量的列表,如 mPendingAnimations,mAnimatorMap 等。要搞清楚这些的含义,还得先搞懂 View.animate()
是支持如何使用的。
这么说吧,仍是拿开头的示例代码来讲明:
mView.animate().scaleX(1.2f).scaleY(1.2f).alpha(0.5f).setDuration(1000).start();
ViewPropertyAnimator 亮点就是支持链式调用一系列不一样的动画一块儿执行,因此须要注意一点,一旦像上述那样使用,那么设定的这一系列动画就会是一块儿执行一块儿结束的。
那么,有可能存在这种场景:先设置了一系列动画执行,若是在这一系列的动画执行结束前,又经过 View.animate()
设置了另一系列一块儿执行的动画效果,那么这时就会有两组动画都在运行中,每组动画均可能含有多种类型的动画,因此内部就须要以每组为单位来保存信息,确保每组动画能够互不干扰,这就是 PropertyBundle 这个类的做用了:
//ViewPropertyAnimator$PropertyBundle private static class PropertyBundle { int mPropertyMask; ArrayList<NameValuesHolder> mNameValuesHolder; ... }
这样解释完,再来看这个类,这样理解两个成员变量的含义就容易多了,首先 mNameValuesHolder 是一个 ArrayList 对象,显然就是用来存储这一组动画里的那一系列不一样类型的动画;那具体存在列表里都有哪些类型的动画呢,就是另外一个成员变量 mPropertyMask 来进行标志了。
而列表里存的这一组动画里的不一样类型的动画,因此 NamaValuesHolder 这个类的做用就是用于区分各类不一样类型的动画了:
//ViewPropertyAnimator$NameValuesHolder static class NameValuesHolder { int mNameConstant; float mFromValue; float mDeltaValue; ... }
第一个成员变量 mNameConstant 就是用于区分不一样类型的动画,在 ViewPropertyAnimator 内部定义了一系列经常使用动画的常量,mNameConstant 这个变量的取值就在这些常量中,如开头出现 SCALE_X。而另外两个变量表示的就是这种类型的动画要进行变化的数值信息。
另外,ViewPropertyAnimator 支持设置一系列不一样类型的动画,那么它是以什么为依据来决定哪一系列的动画做为第一组,哪一系列做为第二组呢?其实很简单,就是以 startAnimation()
被调用为依据。那么,成员变量 mPendingAnimations 的做用也就出来了。
每一次调用 scaleX()
等等之类的方法时,都会建立一个 NameValuesHolder 对象来保存对应这种类型的动画信息,而后保存在 mPendingAnimations 列表中。scaleY()
等这些方法不断被调用,mPendingAnimations 就会保存愈来愈多的待执行的不一样类型的动画。而一旦 startAnimation()
方法被调用时,就会将当前 mPendingAnimations 列表里存的这一系列动画做为同一组一块儿执行一块儿结束的动画保存到一个新的 PropertyBundle 对象里。而后清空 mPendingAnimations,直到下一次 startAnimation()
被调用时,再次将 mPendingAnimations 中新保存的一系列动画做为另一组动画保存到新的 PropertyBundle 中去。
那么,最后还须要有一个变量来保存并区分这一组一组的动画,这就是 mAnimatorMap 变量的做用了。
private HashMap<Animator, PropertyBundle> mAnimatorMap = new HashMap<Animator, PropertyBundle>();
看一下定义,没错吧,PropertyBundle 保存的是一组动画里一块儿开始一块儿结束的一系列动画,因此 mAnimatorMap 是以 Animator 为 Key 区分每一组动画的。
捋清楚了这些内部类和变量的做用,咱们下面再来看以前分析的调用了 scaleX()
后,内部跟到了 animatePropertyBy()
,那么咱们继续跟下去看看:
//ViewPropertyAnimator#animatePropertyBy() private void animatePropertyBy(int constantName, float startValue, float byValue) { //1. mAnimatorMap 存放着一组一组正在运行中的动画 if (mAnimatorMap.size() > 0) { Animator animatorToCancel = null; Set<Animator> animatorSet = mAnimatorMap.keySet(); for (Animator runningAnim : animatorSet) { // 2. bundle 保存着当前这一组动画里的一系列正在运行中的不一样类型的动画 PropertyBundle bundle = mAnimatorMap.get(runningAnim); if (bundle.cancel(constantName)) { if (bundle.mPropertyMask == NONE) { animatorToCancel = runningAnim; break; } } } if (animatorToCancel != null) { animatorToCancel.cancel(); } } // 3. 因此上述1 2步的工做就是要将当前constantName类型的动画取消掉 //4. 建立一个 NameValuesHolder 对象用于保存当前constantName类型的动画信息 NameValuesHolder nameValuePair = new NameValuesHolder(constantName, startValue, byValue); //5. 将该类型的动画信息保存到 mPendingAnimations 中 mPendingAnimations.add(nameValuePair); //6. 安排一个自动开启动画的Runnable,最迟在下一帧触发 mView.removeCallbacks(mAnimationStarter); mView.postOnAnimation(mAnimationStarter); }
上述代码里从第 4-6 的步骤应该都清楚了吧,每次调用 scaleX()
之类的动画,内部须要先建立一个 NameValuesHolder 对象来保存该类型的动画行为(第4步),而后将该类型动画添加到 mPendingAnimations 列表中(第5步)来做为组成一系列一块儿开始一块儿结束的动画,最后会自动安排一个最迟在下一帧内自动启动动画的 Runnable(第6步)。
那么第 1-3 步又是干吗的呢?
是这样的,上面说过,可能会存在一组一组都在运行中的动画,每一组都有一系列不一样类型的动画,那么就有可能出现同一种类型的动画,好比 scaleX()
,既在第一组里,又在第二组里。很显然,ViewPropertyAnimator 里的全部动画都是做用于同一个 View 上,而不一样组的动画又有可能同一时刻都在运行中,那么,一个 View 的同一种类型动画有可能在同一时刻被执行两次么?说得白一点,一个 View 的大小若是在同一帧内先放大 1.2 倍,同时又放大 1.5 倍,那这个 View 呈现出来的效果确定特别错乱。
因此,ViewPropertyAnimator 里全部的动画,在同一时刻,同一类型的动画只支持只有一个处于正在运行中的状态,这也就是第 1-3 步的意义,它须要去遍历当前每一组里的每个动画,若是类型跟当前设定的动画类型一致,那么就将以前的动画取消掉,以最近设定的此次为准。
好了,scaleX()
这些设定动画的行为,内部实现的细节咱们已经分析完了,下面就继续看看下一个流程,启动动画里都干了啥,startAnimation()
:
//ViewPropertyAnimator#startAnimation() private void startAnimation() { //1. 这里我还没搞懂,也不清楚什么场景下会知足这里的条件,直接 return。因此,本篇接下去的分析都是基于假设会直接跳过这里,后面若是搞懂了再来填坑。 if (mRTBackend != null && mRTBackend.startAnimation(this)) { return; } mView.setHasTransientState(true); //2. 建立一个 0.0-1.0 变化的 ValueAnimator ValueAnimator animator = ValueAnimator.ofFloat(1.0f); //3. 将当前 mPengingAnimations 里保存的一系列动画全都取出来,做为同一组一块儿执行一块儿结束的动画 ArrayList<NameValuesHolder> nameValueList = (ArrayList<NameValuesHolder>) mPendingAnimations.clone(); mPendingAnimations.clear(); int propertyMask = 0; int propertyCount = nameValueList.size(); //3.1 遍历这一系列动画,将这些动画都有哪些类型的动画标志出来 for (int i = 0; i < propertyCount; ++i) { NameValuesHolder nameValuesHolder = nameValueList.get(i); propertyMask |= nameValuesHolder.mNameConstant; } //4. 建立一个新的 PropertyBundle 来保存这一组动画,以ValueAnimator做为key来区分 mAnimatorMap.put(animator, new PropertyBundle(propertyMask, nameValueList)); //5. 提供动画开始前,结束后的操做回调 if (mPendingSetupAction != null) { mAnimatorSetupMap.put(animator, mPendingSetupAction); mPendingSetupAction = null; } if (mPendingCleanupAction != null) { mAnimatorCleanupMap.put(animator, mPendingCleanupAction); mPendingCleanupAction = null; } if (mPendingOnStartAction != null) { mAnimatorOnStartMap.put(animator, mPendingOnStartAction); mPendingOnStartAction = null; } if (mPendingOnEndAction != null) { mAnimatorOnEndMap.put(animator, mPendingOnEndAction); mPendingOnEndAction = null; } //6. 对ValueAnimator进行 Listener、StartDelay、Duration、Interpolator 的设置 animator.addUpdateListener(mAnimatorEventListener); animator.addListener(mAnimatorEventListener); if (mStartDelaySet) { animator.setStartDelay(mStartDelay); } if (mDurationSet) { animator.setDuration(mDuration); } if (mInterpolatorSet) { animator.setInterpolator(mInterpolator); } //7. 启用ValueAnimator.start() animator.start(); }
第 1 步我还没搞清楚,就先暂时跳过吧。
第 2-4 步就是咱们上面有说过的,当 startAnimation()
被调用时,将当前保存在 mPendingAnimations 列表里全部的动画都做为同一组一块儿开始一块儿结束的动画,保存到一个新的 PropertyBundle 对象中,每一组动画何时开始,结束,以及每一帧的进度都是借助 ValueAnimator 机制实现,因此每一组动画就以不一样的 ValueAnimator 对象做为 key 值保存到 mAnimatorMap 中相户区分,独立出来。
第 5 步是 ViewPropertyAnimator 支持的接口,都是供外部根据须要使用,好比 mPendingOnStartAction 就是表示会在这一组动画开始的时候被执行,时机跟 onAnimationStart()
相同,外部使用的时候调用 withStartAction()
就能够了。那么为何须要提供这样的接口呢?
这是由于,若是咱们想要在动画开始或结束的时候作一些事,若是咱们是这样使用:
mView.animate().scaleX(1.2f) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { //do something } }).start();
没错,这样写的话,确实能够实如今动画前去执行咱们指定的工做。但这样会有一个问题,由于 ViewPropertyAnimator 动画是支持多组动画同时进行中的,若是像上面这样写的话,那么每一组动画在开始以前就都会去回调这个 onAnimationStart()
方法,去作相同的事。
若是咱们只但愿当前一组动画去执行这些动画开始前的工做,其余组动画不用去执行,那么这时候就可使用 withStartAction()
来实现。
这就是第 5 步的用意。
第 6-7 步也就是对 ValueAnimator 作各类配置,如持续时长,延迟开始时间,插值器等等,最后调用 ValueAnimator.start()
来启动。
好,启动动画的具体的工做咱们也分析完了,剩下最后一个流程了,在每一帧的回调中如何进行 ui 操做而且应用一系列的动画。那么,最后就看看 AnimatorEventListener:
//ViewPropertyAnimator.mAnimatorEventListener private class AnimatorEventListener implements Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { ... @Override public void onAnimationUpdate(ValueAnimator animation) { //1. 取出跟当前 ValueAnimator 绑定的那一组动画 PropertyBundle propertyBundle = mAnimatorMap.get(animation); ... //省略一堆没看懂的代码,跟硬件加速有关 ... //2. 获取 ValueAnimator 机制计算出的当前帧的动画进度 float fraction = animation.getAnimatedFraction(); int propertyMask = propertyBundle.mPropertyMask; ... //3. 遍历这一组动画里的全部动画,分别根据不一样类型的动画进行不一样的 ui 操做来实现动画效果 ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder; if (valueList != null) { int count = valueList.size(); for (int i = 0; i < count; ++i) { //3.1 取出第i个动画 NameValuesHolder values = valueList.get(i); //3.2 根据ValueAnimator计算的当前帧动画进度,以及第i个动画的第一帧的属性值和变化的值来计算出当前帧时的属性值是多少 float value = values.mFromValue + fraction * values.mDeltaValue; //3.3 若是是 alpha 动画,经过View的set方法来修改alpha值,不然调用setValue方法 if (values.mNameConstant == ALPHA) { alphaHandled = mView.setAlphaNoInvalidation(value); } else { setValue(values.mNameConstant, value); } } } //省略alpha动画的一些辅助处理 ... //4. 进度回调,通知外部 if (mUpdateListener != null) { mUpdateListener.onAnimationUpdate(animation); } } }
这个方法作的事也很明确了,上述代码中的注释大概也说完了。也就是说 ViewPropertyAnimator 动画内部在 ValueAnimator 的每一帧回调中,取出跟 ValueAnimator 绑定的那一组动画,以及当前帧的动画进度,而后再遍历当前组的全部动画,分别计算出每一个动画当前帧的属性值,若是不是 alpha 动画的话,直接调用 setValue()
方法来进行 ui 操做达到动画效果,若是是 alpha 动画,则调用 view 的一个 set 方法来实现。
那么,下面再继续看看 setValue()
:
//ViewPropertyAnimator#setValue() private void setValue(int propertyConstant, float value) { final View.TransformationInfo info = mView.mTransformationInfo; final RenderNode renderNode = mView.mRenderNode; switch (propertyConstant) { case TRANSLATION_X: renderNode.setTranslationX(value); break; ... case SCALE_X: renderNode.setScaleX(value); break; case SCALE_Y: renderNode.setScaleY(value); break; ... } }
省略了一堆相似的代码,这个方法里,就所有都是根据不一样类型的动画,取得当前 View 的 mRenderNode 对象,而后分别调用相应的 setXXX 方法,如 SCALE_X 动画,就调用 setScaleX()
方法来进行 ui 操做达到动画效果。
以上,View.animate()
这种方式实现的动画,也就是 ViewPropertyAnimator 动画,的整个流程以及流程里每一个步骤的工做,咱们到此就所有梳理清楚了。
最后,就来进行一下总结:
View.animate()
这种方式实现的动画实际上是 ViewPropertyAnimator 动画。
ViewPropertyAnimator 并非一种动画,它没有继承自 Animator 或者 Animation,它其实只是一个封装类,将经常使用的动画封装起来,对外提供方便使用的接口,内部借助 ValueAnimator 机制。
ViewPropertyAnimator 动画支持自动启动动画,若是外部没有明确调用了 start()
,那么内部会安排一个 Runnable 操做,最迟在下一帧内被执行,这个 Runnable 会去启动动画。
固然,若是外部手动调用了 start()
,那么自动启动动画就没意义了,内部会本身将其取消掉。
ViewPropertyAnimator 对外提供的使用动画的接口很是方便,如 scaleX()
表示 x 的缩放动画,alpha()
表示透明度动画,并且支持链式调用。
因为支持链式调用,因此它支持一系列动画一块儿开始,一块儿执行,一块儿结束。那么当这一系列动画还没执行完又从新发起了另外一系列的动画时,此时两个系列动画就须要分红两组,每一组动画互不干扰,能够同时执行。
但若是同一种类型的动画,如 SCALE_X,在同一帧内分别在多组里都存在,若是都同时运行的话,View 的状态会变得很错乱,因此 ViewPropertyAnimator 规定,同一种类型的动画在同一时刻只能有一个在运行。
也就是说,多组动画能够处于并行状态,可是它们内部的动画是没有交集的,若是有交集,好比 SCALE_X 动画已经在运行中了,可是外部又新设置了一个新的 SCALE_X 动画,那么以前的那个动画就会被取消掉,新的 SCALE_X 动画才会加入新的一组动画中。
因为内部是借助 ValueAnimator 机制,因此在每一帧内均可以接收到回调,在回调中取得 ValueAnimator 计算出的当前帧的动画进度。
取出当前帧的动画进度后,就能够遍历跟当前 ValueAnimator 绑定的那一组动画里全部的动画,分别根据每个动画保存的信息,来计算出当前帧这个动画的属性值,而后调用 View 的 mRenderNode 对象的 setXXX 方法来修改属性值,达到动画效果。
还有一些细节并无概括到总结中,若是只看总结的小伙伴,有时间仍是建议能够慢慢跟着本文过一遍。
Q1:开头说了,使用这种方式实现的动画在某些场景下会有一个坑,这个坑又是什么,是在什么场景下的呢?
开头说过使用这种方式实现的动画,在某些场景下会存在一些坑。原本觉得这篇里也能顺便说清楚,但单单只是原理梳理下来,篇幅就很长了,那么也当作遗留问题,留到以后的文章中来好好说下吧。能够先说下是什么坑:
若是当前界面有使用 RecyclerView 控件,而后又对它的 item 经过 View.animate()
方式实现了一些动画效果,好比很常见的 Tv 应用的主页,界面会有不少卡位,而后每一个卡位得到焦点时通常都须要放大的动画,此时这个卡位就是 RecyclerView 的 item,放大动画能够经过 View.animate()
方式来实现。
在这种场景下,可能会存在这么一种现象,当界面刷新时,若是此时有进行遥控器的方向键按键事件,那么可能会有一些卡位的缩放动画被中断的现象。为何会出现这种现象,再找个时间来梳理清楚。
Q2:View 的 mRenderNode 对象又是个什么东西?它的 setXXX 方法又是如何修改 View 的属性值来达到动画效果的?
还有第二个遗留问题,虽然本篇梳理了 ViewPropertyAnimator 动画的流程和原理,但到最后,咱们其实只知道它内部借助了 ValueAnimator 机制来计算每一帧的动画进度,而后在每一帧的回调中先获取 View 的 mRenderNode 对象,再调用相应的 setXXX 方法来修改属性值达到动画效果。但这个 mRenderNode 是个什么东西,又是如何修改 view 的状态来达到动画效果的这点就还须要找个时间来梳理了。
因此到最后,ViewPropertyAnimator 内部的流程和原理虽然已经清楚了,但具体要不要将这个动画概括到属性动画中,我就不大清楚了。虽然它内部是借助了 ViewAnimator 机制,但 ValueAnimator 其实并无任何的 ui 操做,ObjectAnimator 才会去经过反射来调用相关的 setXXX 方法来修改属性值,这个过程才是 ui 操做,最后才会有相应的动画效果呈现出来。这点还有待继续研究。
最近(2018-03)刚开通了公众号,想激励本身坚持写做下去,初期主要分享原创的Android或Android-Tv方面的小知识,准备可能还有点不足,感兴趣的能够先点一波关注,谢谢支持~~