属性动画 ValueAnimator 运行原理全解析

本篇文章已受权微信公众号 guolin_blog (郭霖)独家发布android

最近下班时间都用来健身还有看书了,博客被晾了一段时间了,原谅我~~~~数组

提问环节

好,废话很少说,以前咱们已经分析过 View 动画 Animation 运行原理解析,那么此次就来学习下属性动画的运行原理。缓存

Q1:咱们知道,Animation 动画内部实际上是经过 ViewRootImpl 来监听下一个屏幕刷新信号,而且当接收到信号时,从 DecorView 开始遍历 View 树的绘制过程当中顺带将 View 绑定的动画执行。那么,属性动画(Animator)原理也是这样么?若是不是,那么它又是怎么实现的?微信

Q2:属性动画(Animator)区别于 Animation 动画的就是它是有对 View 的属性进行修改的,那么它又是怎么实现的,原理又是什么?app

Q3:属性动画(Animator)调用了 start() 以后作了些什么呢?什么时候开始处理当前帧的动画工做?内部又进行了哪些计算呢?ide

基础

属性动画的使用,常接触的其实就是两个类 ValueAnimatorObjectAnimator。其实还有一个 View.animate(),这个内部原理也是属性动画,并且它已经将经常使用的动画封装好了,使用起来很方便,但会有一个坑,咱们留着下一篇来介绍,本篇着重介绍属性动画的运行原理。布局

先看看基本的使用步骤:post

//1.ValueAnimator用法  
ValueAnimator animator = ValueAnimator.ofInt(500);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
               int value = (int) animation.getAnimatedValue();
               mView.setX(value);  
         }
 });
animator.start();

//2.ObjectAnimator用法
ObjectAnimator animator = ObjectAnimator.ofInt(mView, "X", 500).setDuration(1000).start();

这样你就能够看到一个执行了 1s 的平移动画,那么接下去就该开始跟着源码走了,咱们须要梳理清楚,这属性动画是何时开始执行,如何执行的,真正生效的地方在哪里,怎么持续 1s 的,内部是如何进行计算的。学习

在以前分析 Animation 动画运行原理后,咱们也接着分析了 Android 屏幕刷新机制,经过这两篇,咱们知道了 Android 屏幕刷新的关键实际上是 Choreographer 这个类,感兴趣的能够再回去看看,这里提几点里面的结论:动画

咱们知道,Android 每隔 16.6ms 会刷新一次屏幕,也就是每过 16.6ms 底层会发出一个屏幕刷新信号,当咱们的 app 接收到这个屏幕刷新信号时,就会去计算屏幕数据,也就是咱们常说的测量、布局、绘制三大流程。这整个过程关键的一点是,app 须要先向底层注册监听下一个屏幕刷新信号事件,这样当底层发出刷新信号时,才能够找到上层 app 并回调它的方法来通知事件到达了,app 才能够接着去作计算屏幕数据之类的工做。

而注册监听以及提供回调接口供底层调用的这些工做就都是由 Choreographer 来负责,Animation 动画的原理是经过当前 View 树的 ViewRootImpl 的 scheduleTraversals() 方法来实现,这个方法的内部逻辑会走到 Choreographer 里去完成注册监听下一个屏幕刷新信号以及接收到事件以后的工做。

须要跟屏幕刷新信号打交道的话,归根结底最后都是经过 Choreographer 这个类。

那么,当咱们在过属性动画(Animator)的流程源码时,咱们就有一个初步的目标了,至少咱们知道了须要跟踪到 Choreographer 里才能够停下来。至于属性动画的流程原理是跟 Animation 动画流程同样经过 ViewRootImpl 来实现的呢?仍是其余的方式?这些就是咱们此次过源码须要梳理出来的了,那么下面就开始过源码吧。

源码解析

ps:本篇分析的源码基于 android-25 版本,版本不同,源码可能会有些差异,大伙本身过的时候注意一下。

过动画源码的着手点应该都很简单,跟着 start() 一步步追踪下去梳理清楚就能够了。

咱们知道 ObjectAnimator 是继承的 ValueAnimator,那么咱们能够直接从 ValueAnimator 的 start() 开始看,等整个流程梳理清楚后,再回过头看看 ObjectAnimator 的 start() 多作了哪些事就能够了:

ValueAnimator#start.png
很简单,调用了内部的 start(boolean) 方法,

ValueAnimator#start(boolean).png
前面无外乎就是一些变量的初始化,而后好像调用了不少方法,emmm,其实咱们并无必要每一行代码都去搞懂,咱们主要是想梳理整个流程,那么单看方法命名也知道,咱们下面就跟着 startAnimation() 进去看看(但记得,若是后面跟不下去了,要回来这里看看咱们跳过的方法是否是漏掉了一些关键的信息):

ValueAnimator#startAnimation.png
这里调用了两个方法,initAnimation()notifyStartListeners(),感受这两处也只是一些变量的初始化而已,仍是没涉及到流程的信息啊,无论了,也仍是先跟进去确认一下看看:

ValueAnimator#initAnimation.png
确实只是进行一些初始化工做而已,看看另一个:

ValueAnimator#notifyStartListeners.png
这里也只是通知动画开始,回调 listener 的接口而已。

emmm,咱们从 start() 开始一路跟踪下来,发现到目前为止都只是在作动画的一些初始化工做而已,并且跟到这里很明显已是尽头了,下去没有代码了,那么动画初始化以后的下一个步骤究竟是在哪里进行的呢?还记得咱们前面在 start(boolean) 方法里跳过了一些方法么?也许关键就是在这里,那么再回头过去看看:

ValueAnimator#start(boolean)2.png
咱们刚才是根据方法命名,想固然的直接跟着 startAnimation() 走下去了,既然这条路走到底没找到关键信息,那么就折回头看看其余方法。这里调用了 AnimationHandler 类的 addAnimationFrameCallback(),新出现了一个类,看命名应该是专门处理动画相关的,并且仍是单例类,跟进去看看:

AnimationHandler#addAnimationFrameCallback.png
首先第二个参数 delay 取决于咱们是否调用了 setStartDelay() 来设置动画的延迟执行,假设目前的动画都没有设置,那么 delay 也就是 0,因此这里着重看一下前面的代码。

mAnimationCallbacks 是一个 ArrayList,每一项保存的是 AnimationFrameCallback 接口的对象,看命名这是一个回调接口,那么是谁在何时会对它进行回调呢?根据目前仅有的信息,咱们并无办法看出来,那么能够先放着,这里只要记住第一个参数以前传进来的是 this,也就是说若是这个接口被回调时,那么 ValueAnimator 对这个接口的实现将会被回调。

接下去开始按顺序过代码了,当 mAnimationCallbacks 列表大小等于 0 时,将会调用一个方法,很明显,若是动画是第一次执行的话,那么这个列表大小应该就是 0,由于将 callback 对象添加到列表里的操做是在这个判断以后,因此这里咱们能够跟进看看:
AnimationHandler#getProvider.png

MyFrameCallbackProvider#postFrameCallback.png

哇,这么快就看到 Choreographer 了,感受咱们好像已经快接近真相了,继续跟下去:

Choreographer#postFrameCallback.png

Choreographer#postFrameCallbackDelayed.png

因此内部实际上是调用了 postCallbackDelayedInternal() 方法,若是有看过我写的上一篇博客 Android 屏幕刷新机制,到这里是否是已经差很少能够理清了,有时间的能够回去看看,我这里归纳性地给出些结论。

Choreographer 内部有几个队列,上面方法的第一个参数 CALLBACK_ANIMATION 就是用于区分这些队列的,而每一个队列里能够存放 FrameCallback 对象,也能够存放 Runnable 对象。Animation 动画原理上就是经过 ViewRootImpl 生成一个 doTraversal() 的 Runnable 对象(其实也就是遍历 View 树的工做)存放到 Choreographer 的队列里的。而这些队列里的工做,都是用于在接收到屏幕刷新信号时取出来执行的。但有一个关键点,Choreographer 要可以接收到屏幕刷新信号的事件,是须要先调用 Choreographer 的 scheduleVsyncLocked() 方法来向底层注册监听下一个屏幕刷新信号事件的。

而若是继续跟踪 postCallbackDelayedInternal() 这个方法下去的话,你会发现,它最终就是走到了 scheduleVsyncLocked() 里去,这些在上一篇博客 Android 屏幕刷新机制里已经梳理过了,这里就不详细讲了。

那么,到这里,咱们就能够先来梳理一下目前的信息了:

当 ValueAnimator 调用了 start() 方法以后,首先会对一些变量进行初始化工做并通知动画开始了,而后 ValueAnimator 实现了 AnimationFrameCallback 接口,并经过 AnimationHander 将自身 this 做为参数传到 mAnimationCallbacks 列表里缓存起来。而 AnimationHandler 在 mAnimationCallbacks 列表大小为 0 时会经过内部类 MyFrameCallbackProvider 将一个 mFrameCallback 工做缓存到 Choreographer 的待执行队列里,并向底层注册监听下一个屏幕刷新信号事件。

当屏幕刷新信号到的时候,Choreographer 的 doFrame() 会去将这些待执行队列里的工做取出来执行,那么此时也就回调了 AnimationHandler 的 mFrameCallback 工做。

那么到目前为止,咱们可以肯定,当动画第一次调用 start(),这里的第一次应该是指项目里全部的属性动画里某个动画第一次调用 start(),由于 AnimationHandler 是一个单例类,显然是为全部的属性动画服务的。若是是第一次调用了 start(),那么就会去向底层注册监听下一个屏幕刷新信号的事件。因此动画的处理逻辑应该就是在接收到屏幕刷新信号以后回调到的 mFrameCallback 工做里会去间接的调用到的了。

那么,接下去就继续看看,当接收到屏幕刷新信号以后,mFrameCallback 又继续作了什么

AnimationHandler#mFrameCallback.png
其实就作了两件事,一件是去处理动画的相关工做,也就是说要找到动画真正执行的地方,跟着 doAnimationFrame() 往下走应该就好了。而剩下的代码就是处理另一件事:继续向底层注册监听下一个屏幕刷新信号。

先讲讲第二件事,咱们知道,动画是一个持续的过程,也就是说,每一帧都应该处理一个动画进度,直到动画结束。既然这样,咱们就须要在动画结束以前的每个屏幕刷新信号都可以接收到,因此在每一帧里都须要再去向底层注册监听下一个屏幕刷新信号事件。因此你会发现,上面代码里参数是 this,也就是 mFrameCallback 自己,结合一下以前的那个流程,这里能够获得的信息是:

当第一个属性动画调用了 start() 时,因为 mAnimationCallbacks 列表此时大小为 0,因此直接由 addAnimationFrameCallback() 方法内部间接的向底层注册下一个屏幕刷新信号事件,而后将该动画加入到列表里。而当接收到屏幕刷新信号时,mFrameCallback 的 doFrame() 会被回调,该方法内部作了两件事,一是去处理当前帧的动画,二则是根据列表的大小是否不为 0 来决定继续向底层注册监听下一个屏幕刷新信号事件,如此反复,直至列表大小为 0。

因此,这里能够猜想一点,若是当前动画结束了,那么就须要将其从 mAnimationCallbacks 列表中移除,这点能够后面跟源码过程当中来验证。
那么,下去就是跟着 doAnimationFrame() 来看看,属性动画是怎么执行的:

AnimationHandler#doAnimationFrame.png
这里归纳下其实就作了两件事:

一是去循环遍历列表,取出每个 ValueAnimator,而后判断动画是否有设置了延迟开始,或者说动画是否到时间该执行了,若是到时间执行了,那么就会去调用 ValueAnimator 的 doAnimationFrame()

二是调用了 cleanUpList() 方法,看命名就能够猜想是去清理列表,那么应该也就是处理掉已经结束的动画,由于 AnimationHandler 是为全部属性动画服务的,同一时刻也许有多个动画正在进行中,那么动画的结束确定有前后,已经结束的动画确定要从列表中移除,这样等全部动画都结束了,列表大小变成 0 了,mFrameCallback 才能够中止向底层注册监听下一个屏幕刷新信号事件,AnimationHandler 才能够进入空闲状态,不用再每一帧都去处理动画的工做。

那么,咱们优先看看 cleanUpList(),由于感受它的工做比较简单,那就先梳理掉:
AnimationHandler#cleanUpList.png
猜想正确,将列表中为 null 的对象都移除掉,那么咱们就能够继续进一步猜想,动画若是结束的话,会将自身在这个列表中的引用赋值为 null,这点能够在稍微跟踪动画的流程中来进行确认。

清理的工做梳理完,那么接下去就是继续去跟着动画的流程了,还记得咱们上面提到了另外一件事是遍历列表去调用每一个动画 ValueAnimator 的 doAnimationFrame() 来处理动画逻辑么,那么咱们接下去就跟进这个方法看看:

ValueAnimator#doAnimationFrame.png
上面省略了部分代码,省略的那些代码跟动画是否被暂停或从新开始有关,本篇优先梳理正常的动画流程,这些就先不关注了。

稍微归纳一下,这个方法内部其实就作了三件事:
一是处理第一帧动画的一些工做;

二是根据当前时间计算当前帧的动画进度,因此动画的核心应该就是在 animateBaseOnTime() 这个方法里,意义就相似 Animation 动画的 getTransformation()方法;

三是判断动画是否已经结束了,结束了就去调用 endAnimation(),按照咱们以前的猜想,这个方法内应该就是将当前动画从 mAniamtionCallbacks 列表里移除。

咱们先来看动画结束以后的处理工做,由于上面才刚梳理了一部分,趁着如今大伙还有些印象,并且这部分工做会简单易懂点,先把简单的吃掉:
ValueAnimator#endAnimation.png

很简单,两件事,一是去通知说动画结束了,二是调用了 AniamtionHandler 的 removeCallback(),继续跟进看看:

AnimationHandler#removeCallback.png
咱们以前的猜想在这里获得验证了吧,若是动画结束,那么它会将其自身在 AnimationCallbacks 列表里的引用赋值为 null,而后移出列表的工做就交由 AnimationHandler 去作。咱们说了,AnimationHandler 是为全部的属性动画服务的,那么当某个动画结束的话,就必须进行一些资源清理的工做,整个清理的流程逻辑就是咱们目前梳理出来的这样。

好,搞定了一个小点了,那么接下去继续看剩下的两件事,先看第一件,处理动画第一帧的工做问题

参考 Animation 动画的原理,第一帧的工做一般都是为了记录动画第一帧的时间戳,由于后续的每一帧里都须要根据当前时间以及动画第一帧的时间还有一个动画持续时长来计算当前帧动画所处的进度,Animation 动画咱们梳理过了,因此这里在过第一帧的逻辑时应该就会比较有条理点。咱们来看看,属性动画的第一帧的工做是否是跟 Animation 差很少:

ValueAnimator#doAnimationFrame2.png

emmm,看来比 Animation 动画复杂多了,大致上也是干了两件事:

一是调用了 AnimationHandler 的 addOneShotCommitCallback() 方法,具体是干吗的咱们等会来分析;

二就是记录动画第一帧的时间了,mStartTime 变量就是表示第一帧的时间戳,后续的动画进度计算确定须要用到这个变量。至于还有一个 mSeekFraction 变量,它的做用有点相似于咱们用电脑看视频时,能够任意选择从某个进度开始观看。属性动画有提供了一个接口 setCurrentPlayTime()

ValueAnimator animator = ValueAnimator.ofInt(0, 100);
animator.setDuration(4000);
animator.start();

举个例子,。这是一个持续 4s 从 0 增加到 100 的动画,若是咱们调用了 start(),那么 mSeekFraction 默认值是 -1,因此 mStartTime 就是用当前时间做为动画的第一帧时间。若是咱们调用了 setCurrentPlayTime(2000),意思就是说,咱们但愿这个动画从 2s 开始,那么它就是一个持续 2s(4-2) 的从 50 增加到 100 的动画(假设插值器为线性),因此这个时候,mStartTime 就是以比当前时间还早 2s 做为动画的第一帧时间,后面根据 mStartTime 计算动画进度时,就会发现原来动画已通过了 2s 了。

就像咱们看电视时,咱们不想看片头,因此直接选择从正片开始看,相似的道理。

好了,还记得前面说了处理动画第一帧的工做大致上有两件事,另外一件是调用了一个方法么。咱们回头来看看,这里又是作了些什么:
AnimationHandler#addOneShotCommitCallback.png

只是将 ValueAnimator 添加到 AnimationHandler 里的另外一个列表中去,能够过滤这个列表的变量名看看它都在哪些地方被使用到了:

AnimationHandler#doAnimationFrame2.png
这地方还记得吧,咱们上面分析的那一大堆工做都是跟着 callback.doAnimationFrame(frameTime) 这行代码走进去的,虽然内部作的事咱们还没所有分析完,但咱们这里能够知道,等内部全部事都完成后,会退回到 AnimationHandler 的 doAnimationFrame() 继续往下干活,因此再继续跟下去看看:

AnimationHandler#postCommitCallback.png
上面说过,Choreographer 内部有多个队列,每一个队列里均可以存放 FrameCallback 对象,或者 Runnable 对象。此次是传到了另外一个队列里,传进的是一个 Runnable 对象,咱们看看这个 Runnable 作了些什么:

AnimationHandler#commitAnimationFrame.png

ValueAnimator 实现了 AnimationFrameCallback 接口,这里等因而回调了 ValueAnimator 的方法,而后将其从队列中移除。看看 ValueAnimator 的实现作了些什么:

ValueAnimator#commitAnimationFrame.png

好嘛,这里说穿了其实也是在修正动画的第一帧时间 mStartTime。那么,其实也就是说,ValueAnimator 的 doAnimationFrame() 里处理第一帧工做的两件事所有都是用于计算动画的第一帧时间,只是一件是根据是否 "跳过片头"( setCurrentPlayTime()) 计算,另外一件则是这里的修正。

那么,这里为何要对第一帧时间 mStartTime 进行修正呢?

大伙有时间能够去看看 AnimationFrameCallback 接口的 commitAnimationFrame() 方法注释,官方解释得特别清楚了,我这里就不贴图了,直接将个人理解写出来:

其实,这跟属性动画经过 Choreographer 的实现原理有关。咱们知道,屏幕的刷新信号事件都是由 Choreographer 负责,它内部有多个队列,这些队列里存放的工做都是用于在接收到信号时取出来处理。那么,这些队列有什么区别呢?

其实也就是执行的前后顺序的区别,按照执行的前后顺序,咱们假设这些队列的命名为:1队列 > 2队列 > 3队列。咱们本篇分析的属性动画,AnimationHandler 封装的 mFrameCallback 工做就是放到 1队列里的;而以前分析的 Animation 动画,它经过 ViewRootImpl 封装的 doTraversal() 工做是放到 2队列里的;而上面刚过完的修正动画第一帧时间的 Runnable 工做则是放到 3队列里的。

也就是说,当接收到屏幕刷新信号后,属性动画会最早被处理。而后是去计算当前屏幕数据,也就是测量、布局、绘制三大流程。可是这样会有一个问题,若是页面太过复杂,绘制当前界面时花费了太多的时间,那么等到下一个屏幕刷新信号时,属性动画根据以前记录的第一帧时间戳计算动画进度时,会发现丢了开头的好几帧,明明动画没还执行过。因此,这就是为何须要对动画第一帧时间进行修正。

固然,若是动画已经开始了,在动画中间某一帧,就不会去修正了,这个修正,只是针对动画的第一帧时间。由于,若是是在第一帧发现绘制界面太耗时,丢了开头几帧,那么咱们能够经过延后动画开始的时机来达到避免丢帧。但若是是在动画执行过程当中才遇到绘制界面太耗时,那无论什么策略都没法避免丢帧了。


小结1:

好了,到这里,大伙先休息下,咱们来梳理一下目前全部的信息,否则我估计大伙已经忘了上面讲过什么了:

  1. ValueAnimator 属性动画调用了 start() 以后,会先去进行一些初始化工做,包括变量的初始化、通知动画开始事件;

  2. 而后经过 AnimationHandler 将其自身 this 添加到 mAnimationCallbacks 队列里,AnimationHandller 是一个单例类,为全部的属性动画服务,列表里存放着全部正在进行或准备开始的属性动画;

  3. 若是当前存在要运行的动画,那么 AnimationHandler 会去经过 Choreographer 向底层注册监听下一个屏幕刷新信号,当接收到信号时,它的 mFrameCallback 会开始进行工做,工做的内容包括遍历列表来分别处理每一个属性动画在当前帧的行为,处理完列表中的全部动画后,若是列表还不为 0,那么它又会经过 Choreographer 再去向底层注册监听下一个屏幕刷新信号事件,如此反复,直至全部的动画都结束。

  4. AnimationHandler 遍历列表处理动画是在 doAnimationFrame() 中进行,而具体每一个动画的处理逻辑则是在各自,也就是 ValueAnimator 的 doAnimationFrame() 中进行,各个动画若是处理完自身的工做后发现动画已经结束了,那么会将其在列表中的引用赋值为空,AnimationHandler 最后会去将列表中全部为 null 的都移除掉,来清理资源。

  5. 每一个动画 ValueAnimator 在处理自身的动画行为时,首先,若是当前是动画的第一帧,那么会根据是否有"跳过片头"(setCurrentPlayTime())来记录当前动画第一帧的时间 mStartTime 应该是什么。

  6. 第一帧的动画其实也就是记录 mStartTime 的时间以及一些变量的初始化而已,动画进度仍然是 0,因此下一帧才是动画开始的关键,但因为属性动画的处理工做是在绘制界面以前的,那么有可能由于绘制耗时,而致使 mStartTime 记录的第一帧时间与第二帧之间隔得过久,形成丢了开头的多帧,因此若是是这种状况下,会进行 mStartTime 的修正。

  7. 修正的具体作法则是当绘制工做完成后,此时,再根据当前时间与 mStartTime 记录的时间作比较,而后进行修正。

  8. 若是是在动画过程当中的某一帧才出现绘制耗时现象,那么,只能表示无能为力了,丢帧是避免不了的了,想要解决就得本身去分析下为何绘制会耗时;而若是是在第一帧是出现绘制耗时,那么,系统仍是能够帮忙补救一下,修正下 mStartTime 来达到避免丢帧。

好了,休息结束,咱们继续,还有一段路要走,其实整个流程目前大致上已经出来了,只是缺乏了当前帧的动画进度具体计算实现细节,这部分估计会更让人头大。

以前分析 ValueAnimator 的 doAnimationFrame() 时,咱们将其归纳出来主要作了三件事:一是处理第一帧动画的工做;二是根据当前时间计算并实现当年帧的动画工做;三是根据动画是否结束进行一些资源清理工做;一三咱们都分析了,下面就来过过第二件事,animateBasedOnTime()

ValueAnimator#animateBaseOnTime.png

从这里开始,就是在计算当前帧的动画逻辑了,整个过程跟 Animation 动画基本上差很少。上面的代码里,我省略了一部分,那部分是用于根据是否设置的 mRepeatCount 来处理动画结束后是否须要从新开始,这些咱们就不看了,咱们着重梳理一个正常的流程下来便可。

因此,归纳一下,这个方法里其实也就是作了三件事:

一是,根据当前时间以及动画第一帧时间还有动画持续的时长来计算当前的动画进度。

二是,确保这个动画进度的取值在 0-1 之间,这里调用了两个方法来辅助计算,咱们就不跟进去了,之因此有这么多的辅助计算,那是由于,属性动画支持 setRepeatCount() 来设置动画的循环次数,而从始至终的动画第一帧的时间都是 mStrtTime 一个值,因此在第一个步骤中根据当前时间计算动画进度时会发现进度值是可能会超过 1 的,好比 1.5, 2.5, 3.5 等等,因此第二个步骤的辅助计算,就是将这些值等价换算到 0-1 之间。

三就是最重要的了,当前帧的动画进度计算完毕以后,就是须要应用到动画效果上面了,因此 animateValue() 方法的意义就是相似于 Animation 动画中的 applyTransformation()

咱们都说,属性动画是经过修改属性值来达到动画效果的,那么咱们就跟着 animateValue() 进去看看:

ValueAnimator#animateValue.png

这里干的活我也大概的给划分红了三件事:

一是,根据插值器来计算当前的真正的动画进度,插值器算是动画里比较重要的一个概念了,可能平时用的少,若是咱们没有明确指定使用哪一个插值器,那么系统一般会有一个默认的插值器。

二是,根据插值器计算获得的实际动画进度值,来映射到咱们须要的数值。这么说吧,就算通过了插值器计算以后,动画进度值也只是 0-1 区间内的某个值而已。而咱们一般须要的并非 0-1 的数值,好比咱们但愿一个 0-500 的变化,那么咱们就须要本身在拿到 0-1 区间的进度值后来进行转换。第二个步骤,大致上的工做就是帮助咱们处理这个工做,咱们只须要告诉 ValueAnimator 咱们须要 0-500 的变化,那么它在拿到进度值后会进行转换。

三就只是通知动画的进度回调而已了。

流程上差很少已经梳理出来了,不过我我的对于内部是如何根据拿到的 0-1 区间的进度值转换成咱们指定区间的数值的工做挺感兴趣的,那么咱们就稍微再深刻去分析一下好了。这部分工做主要就是调用了 mValues[i].calculateValue(fraction) 这一行代码来实现,mValues 是一个 PropertyValuesHolder 类型的数组,因此关键就是去看看这个类的 calculateValue() 作了啥:

PropertyValuesHolder#calculateValue.png

咱们在使用 ValueAnimator 时,注册了动画进度回调,而后在回调里取当前的值时其实也就是取到上面那个 mAnimatedValue 变量的值,而这个变量的值是经过 mKeyframes.getValue() 计算出来的,那么再继续跟进看看:

KeyFrames#getValue.png

KeyFrames 是一个接口,那么接下去就是要找找哪里实现了这个接口:

PropertyValuesHolder#setIntValues.png

具体的找法,能够在 PropertyValuesHolder 这个类里利用 Ctrl + F 过滤一下 mKeyframes =来看一下它在哪些地方被实例化了。匹配到的地方不少,但都差很少,都是经过 KeyframeSet 的 ofXXX 方法实例化获得的对象,那么具体的实现应该就是在 KeyframeSet 这个类里了。

在跟进去看以前,有一点想提一下,大伙应该注意到了吧,mKeyframes 实例化的这些地方,ofInt()onFloat() 等等是否是很熟悉。没错,就是咱们建立属性动画时类似的方法名, 其实 ValueAnimator.ofInt() 内部会根据相应的方法来建立 mKeyframes 对象,也就是说,在实例化属性动画时,这些 mKeyframes 也顺便被实例化了。想确认的,大伙能够本身去跟下源码看看,我这里就不贴了。

好了,接下去看看 KeyframeSet 这个类的 ofInt() 方法,看看它内部具体是建立了什么:

KeyframeSet#ofInt.png

这里又涉及到新的机制了吧,Keyframe,KeyframeSet,Keyframes 这些大伙感兴趣能够去查查看,我也没有深刻去了解。但看了别人的一些介绍,这里大概讲一下。直接从翻译上来看,这个也就是指关键帧,就像一部电影由多帧画面组成同样的道理,动画也是由一帧帧组成的。

还记得,咱们为啥会跟到这里来了么。动画在处理当前帧的工做时,会去计算当前帧的动画进度,而后根据这个 0-1 区间的进度,映射到咱们须要的数值,而这个映射以后的数值就是经过 mKeyframes 的 getValue() 里取到的,mKeyframes 是一个 KeyframeSet 对象,在建立属性动画时也顺带被建立了,而建立属性动画时,咱们会传入一个咱们想要的数值,如 ValueAnimator.ofInt(100) 就表示咱们想要的动画变化范围是 0-100,那么这个 100 在内部也会被传给 KeyframeSet.ofInt(100),而后就是进入到上面代码块里的建立工做了。

在这个方法里,100 就是做为一个关键帧。那么,对于一个动画来讲,什么才叫作关键帧呢?很明显,至少动画须要知道从哪开始,到哪结束,是吧?因此,对于一个动画来讲,至少须要两个关键帧,若是咱们调用 ofInt(100) 只传进来一个数值时,那么内部它就默认认为起点是从 0 开始,传进来的 100 就是结束的关键帧,因此内部就会本身建立了两个关键帧。

那么,这些关键帧又是怎么被动画用上的呢?这就是回到咱们最初跟踪的 mKeyframes.getValue() 这个方法里去了,看上面的代码块,KeyframeSet.ofInt() 最后是建立了一个 IntKeyframeSet 对象,因此咱们跟进这个类的 getValue() 方法里看看它是怎么使用这些关键帧的:

IntKeyframeSet#getValue.png

因此关键的工做就都在 getIntValue() 这里了,参数传进来还记得是什么吧,就是通过插值器计算以后当前帧的动画进度值,0-1 区间的那个值,getIntValue() 这个方法的代码有些多,咱们一块一块来看,先看第一块:

IntKeyframeSet#getIntValue.png

当关键帧只有两帧时,咱们常使用的 ValueAnimator.ofInt(100), 内部其实就是只建立了两个关键帧,一个是起点 0,一个是结束点 100。那么,在这种只有两帧的状况下,将 0-1 的动画进度值转换成咱们须要的 0-100 区间内的值,系统的处理很简单,若是没有设置估值器,也就是 mEvaluator,那么就直接是按比例来转换,好比进度为 0.5,那按比例转换就是 (100 - 0) * 0.5 = 50。若是有设置估值器,那就按照估值器定的规则来,估值器其实就是相似于插值器,属性动画里才引入的概念,Animation 动画并无,由于只有属性动画内部才帮咱们作了值转换工做。

上面是当关键帧只有两帧时的处理逻辑,那么当关键帧超过两帧的时候呢:
IntKeyframeSet#getIntValue2.png
当关键帧超过两帧时,分三种状况来处理:第一帧的处理;最后一帧的处理;中间帧的处理;

那么,何时关键帧会超过两帧呢?其实也就是咱们这么使用的时候:ValueAnimator.ofInt(0, 100, 0, -100, 0),相似这种用法的时候关键帧就不止两个了,这时候数量就是根据参数的个数来决定的了。

那么,咱们再来详细看看三种状况的处理逻辑,首先是第一帧的处理逻辑:

IntKeyframeSet#getIntValue3.png

fraction <= 0f 表示的应该不止是第一帧的意思,但除了理解成第一帧外,我不清楚其余场景是什么,暂时以第一帧来理解,这个应该影响不大。

处理的逻辑其实也很简单,还记得当只有两个关键帧时是怎么处理的吧。那在处理第一帧的工做时,只须要将第二帧当成是最后一帧,那么第一帧和第二帧这样也就能够当作是只有两帧的场景了吧。可是参数 fraction 动画进度是以实际第一帧到最后一帧计算出来的,因此须要先对它进行转换,换算出它在第一帧到第二帧之间的进度,接下去的逻辑也就跟处理两帧时的逻辑是同样的了。

一样的道理,在处理最后一帧时,只须要取出倒数第一帧跟倒数第二帧的信息,而后将进度换算到这两针之间的进度,接下去的处理逻辑也就是同样的了。代码我就不贴了。

但处理中间帧的逻辑就不同了,由于根据 0-1 的动画进度,咱们能够很容易区分是处于第一帧仍是最后一帧,无非一个就是 0,一个是 1。可是,当动画进度值在 0-1 之间时,咱们并无办法直接看出这个进度值是落在中间的哪两个关键帧之间,若是有办法计算出当前的动画进度处于哪两个关键帧之间,那么接下去的逻辑也就是同样的了,因此关键就是在于找出当前进度处于哪两个关键帧之间:

IntKeyframeSet#getIntValue4.png

系统的找法也很简单,从第一帧开始,按顺序遍历每一帧,而后去判断当前的动画进度跟这一帧保存的位置信息来找出当前进度是否就是落在某两个关键帧之间。由于每一个关键帧保存的信息除了有它对应的值以外,还有一个是它在第一帧到最后一帧之间的哪一个位置,至于这个位置的取值是什么,这就是由在建立这一系列关键帧时来控制的了。

还记得是在哪里建立了这一系列的关键帧的吧,回去 KeyframeSet 的 ofInt() 里看看:
KeyframeSet#ofInt2.png

在建立每一个关键帧时,传入了两个参数,第一个参数就是表示这个关键帧在整个区域之间的位置,第二参数就是它表示的值是多少。看上面的代码, i 表示的是第几帧,numKeyframes 表示的是关键帧的总数量,因此 i/(numKeyframes - 1) 也就是表示这一系列关键帧是按等比例来分配的。

好比说, ValueAnimator.ofInt(0, 50, 100, 200),这总共有四个关键帧,那么按等比例分配,第一帧就是在起点位置 0,第二帧在 1/3 位置,第三帧在 2/3 的位置,最后一帧就是在 1 的位置。


小结2:

到这里,咱们再来梳理一下后面部分过的内容:

  1. 当接收到屏幕刷新信号后,AnimationHandler 会去遍历列表,将全部待执行的属性动画都取出来去计算当前帧的动画行为。

  2. 每一个动画在处理当前帧的动画逻辑时,首先会先根据当前时间和动画第一帧时间以及动画的持续时长来初步计算出当前帧时动画所处的进度,而后会将这个进度值等价转换到 0-1 区间以内。

  3. 接着,插值器会将这个通过初步计算以后的进度值根据设定的规则计算出实际的动画进度值,取值也是在 0-1 区间内。

  4. 计算出当前帧动画的实际进度以后,会将这个进度值交给关键帧机制,来换算出咱们须要的值,好比 ValueAnimator.ofInt(0, 100) 表示咱们须要的值变化范围是从 0-100,那么插值器计算出的进度值是 0-1 之间的,接下去就须要借助关键帧机制来映射到 0-100 之间。

  5. 关键帧的数量是由 ValueAnimator.ofInt(0, 1, 2, 3) 参数的数量来决定的,好比这个就有四个关键帧,第一帧和最后一帧是必须的,因此最少会有两个关键帧,若是参数只有一个,那么第一帧默认为 0,最后一帧就是参数的值。当调用了这个 ofInt() 方法时,关键帧组也就被建立了。

  6. 当只有两个关键帧时,映射的规则是,若是没有设置估值器,那么就等比例映射,好比动画进度为 0.5,须要的值变化区间是 0-100,那么等比例映射后的值就是 50,那么咱们在 onAnimationUpdate 的回调中经过 animation.getAnimatedValue() 获取到的值 50 就是这么来的。

  7. 若是有设置估值器,那么就按估值器的规则来进行映射。

  8. 当关键帧超过两个时,须要先找到当前动画进度是落于哪两个关键帧之间,而后将这个进度值先映射到这两个关键帧之间的取值,接着就能够将这两个关键帧当作是第一帧和最后一帧,那么就能够按照只有两个关键帧的状况下的映射规则来进行计算了。

  9. 而进度值映射到两个关键帧之间的取值,这就须要知道每一个关键帧在整个关键帧组中的位置信息,或者说权重。而这个位置信息是在建立每一个关键帧时就传进来的。onInt() 的规则是全部关键帧按等比例来分配权重,好比有三个关键帧,第一帧是 0,那么第二帧就是 0.5, 最后一帧 1。

至此,咱们已经将整个流程梳理出来了,两部分小结的内容整合起来就是此次梳理出来的整个属性动画从 start() 以后,到咱们在 onAnimationUpdate 回调中取到咱们须要的值,再到动画结束后如何清理资源的整个过程当中的原理解析。

梳理清楚后,大伙应该就要清楚,属性动画是如何接收到屏幕刷新信号事件的?是如何反复接收到屏幕刷新信号事件直到整个动画执行结束?方式是不是有区别于 Animation 动画的?计算当前帧的动画工做都包括了哪些?是如何将 0-1 的动画进度映射到咱们须要的值上面的?

若是看完本篇,这些问题你内心都有谱了,那么就说明,本篇的主要内容你都吸取进去了。固然,若是有错的地方,欢迎指出来,毕竟内容确实不少,颇有可能存在写错的地方没发现。

来张时序图结尾:

VauleAnimatior运行原理时序图.png

最后,有一点想提的是,咱们本篇只是过完了 ValueAnimator 的整个流程原理,但这整个过程当中,注意到了没有,咱们并无看到有任何一个地方涉及到了 ui 操做。在上一篇博客 Android 屏幕刷新机制中,咱们也清楚了,界面的绘制其实就是交由 ViewRootImpl 来发起的,但很显然,ValueAnimator 跟 ViewRootImpl 并无任何交集。

那么,ValueAnimator 又是怎么实现动画效果的呢?其实,ValueAnimator 只是按照咱们设定的变化区间(ofInt(0, 100)),持续时长(setDuration(1000)),插值器规则,估值器规则,内部在每一帧内经过一系列计算,转换等工做,最后输出每一帧一个数值而已。而若是要实现一个动画效果,那么咱们只能在进度回调接口取到这个输出的值,而后手动应用到某个 View 上面(mView.setX())。因此,这种使用方式,本质上仍然是经过 View 的内部方法最终走到 ViewRootImpl 去触发界面的更新绘制。

而 ObjectAnimator 却又不一样了,它内部就有涉及到 ui 的操做,具体原理是什么,留待后续再分析。

遗留问题

都说属性动画是经过改变属性值来达到动画效果的,计划写这一篇时,原本觉得能够梳理清楚这点的,谁知道单单只是把 ValueAnimator 的流程原理梳理出来篇幅就这么长了,因此 ObjectAnimator 就另找时间再来梳理吧,这个问题就做为遗留问题了。

Q1:都说属性动画是经过改变属性值来达到动画效果的,那么它的原理是什么呢?


QQ图片20180316094923.jpg 最近(2018.03)刚开通了公众号,想激励本身坚持写做下去,初期主要分享原创的Android或Android-Tv方面的小知识,感兴趣的能够点一波关注,谢谢支持~~

相关文章
相关标签/搜索