View 动画 Animation 运行原理解析

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

此次想来梳理一下 View 动画也就是补间动画(ScaleAnimation, AlphaAnimation, TranslationAnimation...)这些动画运行的流程解析。内容并不会去分析动画的呈现原理是什么,诸如 Matrix 这类的原理是什么,由于我也还没搞懂。本篇主要是分析当调用了 View.startAnimation() 以后,动画从开始到结束的一个运行流程是什么?微信

提问环节

看源码最好是带着问题去,这样比较有目的性和针对性,能够防止阅读源码时走偏和钻牛角,因此咱们就先来提几个问题。app

Animation 动画的扩展性很高,系统只是简单的为咱们封装了几个基本的动画:平移、旋转、透明度、缩放等等,感兴趣的能够去看看这几个动画的源码,它们都是继承自 Animation 类,而后实现了 applyTransformation() 方法,在这个方法里经过 Transformation 和 Matrix 实现各类各样炫酷的动画,因此,若是想要作出炫酷的动画效果,这些仍是须要去搞懂的。源码分析

目前我也还没搞懂,能力有限,因此优先分析动画的一个运行流程。布局

首先看看 Animation 动画的基本用法:
基本用法动画

咱们要使用一个 View 动画时,通常都是先 new 一个动画,而后配置各类参数,最后调用动画要做用到的那个 View 的 startAnimation(), 将动画实例做为参数传进去,接下去就能够看到动画运行的效果了。this

那么,问题来了:spa

Q1:不知道大伙想过没有,当调用了 View.startAnimation() 以后,动画是立刻就执行了么?3d

Q2:假如动画持续时间 300ms,当调用了 View.startAniamtion() 以后,又发起了一次界面刷新的操做,那么界面的刷新是在 300ms 以后也就是动画执行完毕以后才执行的,仍是在动画执行过程当中界面刷新操做就执行了呢?日志

咱们都知道,applyTransformation() 这个方法是动画生效的地方,这个方法被回调时参数会传进来当前动画的进度(0.0 ——— 1.0)。就像数学上的画曲线,当给的点越多时画的曲线越光滑,一样当这个方法被回调越屡次时,动画的效果越流畅。

好比一个从 0 放大到 1280 的 View 放大动画,若是这过程该方法只回调 3 次的话,那么每次的跨度就会很大,好比 0 —— 600 —— 1280,那么这个动画效果看起来就会很突兀;相反,若是这过程该方法回调了几十次的话,那么每次跨度可能就只有 100,这样一来动画效果看起来就会很流畅。

相信大伙也都有过在 applyTransformation() 里打日志来查看当前的动画进度,有时打出的日志有十几条,有时却又有几十条。

那么咱们的问题就来了:

Q3:applyTransformation() 这个方法的回调次数是根据什么来决定的?

好了,本篇就是主要讲解这三个问题,这三个问题搞明白的话,之后碰到动画卡顿的时候就懂得如何去分析、定位丢帧的地方了,找到丢帧的问题所在后离解决问题也就不远了。

源码分析

ps:本篇分析的源码全都基于 android-25 版本。如下源码均采用截图方式,每张图最上面是类名+方法名,大伙想本身过一遍的时候,若是不清楚方法属于哪一个类的能够在每张图最上面查看。

View.startAnimation()

刚开始接触源码分析可能不清楚该从哪入手,建议能够从咱们使用它的地方来 startAnimation()
startAnimation.png

代码很少,调用了四个方法,那么一个个跟进去看看,先是 setStartTime()
setStartTime.png

因此这里只是对一些变量进行赋值,并无运行动画的逻辑,继续看看 setAnimation()
setAnimation.png

View 里面有一个 Animation 类型的成员变量,因此这个方法实际上是将咱们 new 的 ScaleAnimation 动画跟 View 绑定起来而已,也没有运行动画的逻辑,继续往下看看 invalidateParentCached()
invalidateParentCached.png

invalidateParentCaches() 这方法更简单,给 mPrivateFlags 添加了一个标志位,虽然还不清楚干吗的,但能够先留个心眼,由于 mPrivateFlags 这个变量在阅读跟 View 相关的源码时常常碰到,那么能够的话能搞明白就搞明白,但目前跟咱们想要找出动画到底何时开始执行的关系好像不大,先略过,继续跟进 invalidate()
invalidateInternal.png

因此 invalidate() 内部实际上是调用了 ViewGroup 的 invalidateChild(),再跟进看看:
invalidateChild.png

这里有一个 do{}while() 的循环操做,第一次循环的时候 parent 是 this,即 ViewGroup 自己,因此接下去就是调用 ViewGroup 自己的 invalidateChildInParent() 方法,而后循环终止条件是 patent == null,因此能够猜想这个方法返回的应该是 ViewGroup 的 parent,跟进看看:
invalidateChildInparent.png

因此关键是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 这两个是何时赋值给 mPrivateFlags,由于只要有两个标志中的一个时,该方法就会返回 mParent,具体赋值的地方还不大清楚,但能肯定的是动画执行时,它是知足 if 条件的,也就是这个方法会返回 mParent。

一个具体的 View 的 mParent 是 ViewGroup,ViewGroup 的 mParent 也是 ViewGoup,因此在 do{}while() 循环里会一直不断的寻找 mParent,而一颗 View 树最顶端的 mParent 是 ViewRootImpl,因此最终是会走到了 ViewRootImpl 的 invalidateChildInParent() 里去了。

至于一个界面的 View 树最顶端为何是 ViewRootImpl,这个就跟 Activity 启动过程有关了。咱们都清楚,在 onCreate 里 setContentView() 的时候,是将咱们本身写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,也就是说 DevorView 才是 View 树的根布局,那为何又说 View 树最顶端实际上是 ViewRootImpl 呢?

这是由于在 onResume() 执行完后,WindowManager 将会执行 addView(),而后在这里面会去建立一个 ViewRootImpl 对象,接着将 DecorView 跟 ViewRootImpl 对象绑定起来,而且将 DecorView 的 mParent 设置成 ViewRootImpl,而 ViewRootImpl 是实现了 ViewParent 接口的,因此虽然 ViewRootImpl 没有继承 View 或 ViewGroup,但它确实是 DecorView 的 parent。这部份内容应该属于 Activity 的启动过程相关原理的,因此本篇只给出结论,不深刻分析了,感兴趣的能够自行搜索一下。

那么咱们继续返回到寻找动画执行的地方,咱们跟到了 ViewRootImpl 的 invalidateChildInParent() 里去了,看看它作了些什么:
ViewRootImpl#invalidateChildInParent.png

首先第一点,它的全部返回值都是 null,因此以前那个 do{}while() 循环最终就是执行到这里后确定就会中止了。而后参数 dirty 是在最初 View 的 invalidateInternal() 里层层传递过来的,能够确定的是它不为空,也不是 isEmpty,因此继续跟到 invalidateRectOnScreen() 方法里看看:
invalidateRectOnScreen.png

跟到这里就能够了,scheduleTraversals() 做用是将 performTraversals() 封装到一个 Runnable 里面,而后扔到 Choreographer 的待执行队列里,这些待执行的 Runnable 将会在最近的一个 16.6 ms 屏幕刷新信号到来的时候被执行。而 performTraversals() 是 View 的三大操做:测量、布局、绘制的发起者。

View 树里面无论哪一个 View 发起了布局请求、绘制请求,通通最终都会走到 ViewRootImpl 里的 scheduleTraversals(),而后在最近的一个屏幕刷新信号到了的时候再经过 ViewRootImpl 的 performTraversals() 从根布局 DecorView 开始依次遍历 View 树去执行测量、布局、绘制三大操做。这也是为何一直要求页面布局层次不能太深,由于每一次的页面刷新都会先走到 ViewRootImpl 里,而后再层层遍历到具体发生改变的 View 里去执行相应的布局或绘制操做。

这些内容应该是属于 Android 屏幕刷新机制的,这里就先只给出结论,具体分析我会在几天后再发一篇博客出来。

因此,咱们从 View.startAnimation() 开始跟进源码分析的这一过程当中,也能够看出,执行动画,其实内部会调用 View 的重绘请求操做 invalidate() ,因此最终会走到 ViewRootImpl 的 scheduleTraversals(),而后在下一个屏幕刷新信号到的时候去遍历 View 树刷新屏幕。

因此,到这里能够获得的结论是:

当调用了 View.startAniamtion() 以后,动画并无立刻就被执行,这个方法只是作了一些变量初始化操做,接着将 View 和 Animation 绑定起来,而后调用重绘请求操做,内部层层寻找 mParent,最终走到 ViewRootImpl 的 scheduleTraversals 里发起一个遍历 View 树的请求,这个请求会在最近的一个屏幕刷新信号到来的时候被执行,调用 performTraversals 从根布局 DecorView 开始遍历 View 树。

动画真正执行的地方

那么,到这里,咱们能够猜想,动画其实真正执行的地方应该是在 ViewRootImpl 发起的遍历 View 树的这个过程当中。测量、布局、绘制,View 显示到屏幕上的三个基本操做都是由 ViewRootImpl 的 performTraversals() 来控制,而做为 View 树最顶端的 parent,要控制这颗 Veiw 树的三个基本操做,只能经过层层遍历。因此,测量、布局、绘制三个基本操做的执行都会是一次遍历操做。

我在跟着这三个流程走的时候,最后发现,在跟着绘制流程走的时候,看到了跟动画相关的代码,因此咱们就跳过其余两个流程,直接看绘制流程:

绘制流程.png

这张图不是我画的,在网上找的,绘制流程的开始是由 ViewRootImpl 发起的,而后从 DecorView 开始遍历 View 树。而遍历的实现,是在 View#draw() 方法里的。咱们能够看看这个方法的注释:
draw.png

这个方法里主要作了上述六件事,大致上就是若是当前 View 须要绘制,就会去调用本身的 onDraw(),而后若是有子 View,就会调用dispatchDraw() 将绘制事件通知给子 View。ViewGroup 重写了 dispatchDraw(),调用了 drawChild(),而 drawChild() 调用了子 View 的 draw(Canvas, ViewGroup, long),而这个方法又会去调用到 draw(Canvas) 方法,因此这样就达到了遍历的效果。整个流程就像上上图中画的那样。

在这个流程中,当跟到 draw(Canvas, ViewGroup, long) 里时,发现了跟动画相关的代码:
draw2.png

还记得咱们调用 View.startAnimation(Animation) 时将传进来的 Animation 赋值给 mCurrentAnimation 了么。
getAnimation.png

因此当时传进来的 Animation ,如今拿出来用了,那么动画真正执行的地方应该也就是在 applyLegacyAnimation() 方法里了(该方法在 android-22 版本及以前的命名是 drawAnimation)
applyLegacyAnimation.png

这下肯定动画真正开始执行是在什么地方了吧,都看到 onAnimationStart() 了,也看到了对动画进行初始化,以及调用了 Animation 的 getTransformation,这个方法是动画的核心,再跟进去看看:
getTransformation.png

这个方法里作了几件事:

  1. 记录动画第一帧的时间
  2. 根据当前时间到动画第一帧的时间这之间的时长和动画应持续的时长来计算动画的进度
  3. 把动画进度控制在 0-1 之间,超过 1 的表示动画已经结束,从新赋值为 1 便可
  4. 根据插值器来计算动画的实际进度
  5. 调用 applyTransformation() 应用动画效果

因此,到这里咱们已经能肯定 applyTransformation() 是何时回调的,动画是何时才真正开始执行的。那么 Q1 总算是搞定了,Q2 也基本能理清了。由于咱们清楚, applyTransformation() 最终是在绘制流程中的 draw() 过程当中执行到的,那么显然在每一帧的屏幕刷新信号来的时候,遍历 View 树是为了从新计算屏幕数据,也就是所谓的 View 的刷新,而动画只是在这个过程当中顺便执行的。

接下去就是 Q3 了,咱们知道 applyTransformation() 是动画生效的地方,这个方法不断的被回调时,参数会传进来动画的进度,因此呈现效果就是动画根据进度在运行中。

可是,咱们从头分析下来,找到了动画真正执行的地方,找到了 applyTransformation() 被调用的地方,但这些地方都没有看到任何一个 for 或者 while 循环啊,也就是一次 View 树的遍历绘制操做,动画也就只会执行一次而已啊?那么它是怎么被回调那么屡次的?

咱们知道 applyTransformation() 是在 getTransformation() 里被调用的,而这个方法是有一个 boolean 返回值的,咱们看看它的返回逻辑是什么:
getTransformation2.png

也就是说 getTransformation() 的返回值表明的是动画是否完成,还记得是哪里调用的 getTransformation() 吧,去 applyLegacyAnimation() 里看看取到这个返回值后又作了什么:
applyLegacyAnimation2.png

当动画若是还没执行完,就会再调用 invalidate() 方法,层层通知到 ViewRootImpl 再次发起一次遍历请求,当下一帧屏幕刷新信号来的时候,再经过 performTraversals() 遍历 View 树绘制时,该 View 的 draw 收到通知被调用时,会再次去调用 applyLegacyAnimation() 方法去执行动画相关操做,包括调用 getTransformation() 计算动画进度,调用 applyTransformation() 应用动画。

也就是说,动画很流畅的状况下,实际上是每隔 16.6ms 即每一帧到来的时候,执行一次 applyTransformation(),直到动画完成。因此这个 applyTransformation() 被回调屡次是这么来的,并且这个回调次数并无办法人为进行设定。

这就是为何当动画持续时长越长时,这个方法打出的日志越屡次的缘由。

还记得 getTransformation() 方法在计算动画进度时是根据参数传进来的 currentTime 的么,而这个 currentTime 能够理解成是发起遍历操做这个时刻的系统时间(实际 currentTime 是在 Choreographer 的 doFrame() 里通过校验调整以后的一个时间,但离发起遍历操做这个时刻的系统时间相差很小,因此不深究的话,能够像上面那样理解,比较容易明白)。

小结

综上,咱们稍微整理一下:

  1. 首先,当调用了 View.startAnimation() 时动画并无立刻就执行,而是经过 invalidate() 层层通知到 ViewRootImpl 发起一次遍历 View 树的请求,而此次请求会等到接收到最近一帧到了的信号时才去发起遍历 View 树绘制操做。

  2. 从 DecorView 开始遍历,绘制流程在遍历时会调用到 View 的 draw() 方法,当该方法被调用时,若是 View 有绑定动画,那么会去调用applyLegacyAnimation(),这个方法是专门用来处理动画相关逻辑的。

  3. 在 applyLegacyAnimation() 这个方法里,若是动画尚未执行过初始化,先调用动画的初始化方法 initialized(),同时调用 onAnimationStart() 通知动画开始了,而后调用 getTransformation() 来根据当前时间计算动画进度,紧接着调用 applyTransformation() 并传入动画进度来应用动画。

  4. getTransformation() 这个方法有返回值,若是动画还没结束会返回 true,动画已经结束或者被取消了返回 false。因此 applyLegacyAnimation() 会根据 getTransformation() 的返回值来决定是否通知 ViewRootImpl 再发起一次遍历请求,返回值是 true 表示动画没结束,那么就去通知 ViewRootImpl 再次发起一次遍历请求。而后当下一帧到来时,再从 DecorView 开始遍历 View 树绘制,重复上面的步骤,这样直到动画结束。

  5. 有一点须要注意,动画是在每一帧的绘制流程里被执行,因此动画并非单独执行的,也就是说,若是这一帧里有一些 View 须要重绘,那么这些工做一样是在这一帧里的此次遍历 View 树的过程当中完成的。每一帧只会发起一次 perfromTraversals() 操做。

以上,就是本篇全部的内容,将 View 动画 Animation 的运行流程原理梳理清楚,但要搞清楚为何动画会出现卡顿现象的话,还须要理解 Android 屏幕的刷新机制以及消息驱动机制;这些内容将在最近几天内整理成博客分享出来。

遗留问题

最后仍然遗留一些还没有解决的问题,等待继续探索:

Q1:大伙都清楚,View 动画区别于属性动画的就是 View 动画并不会对这个 View 的属性值作修改,好比平移动画,平移以后 View 仍是在原来的位置上,实际位置并不会随动画的执行而移动,那么这点的原理是什么?

Q2:既然 View 动画不会改变 View 的属性值,那么若是是缩放动画时,View 须要从新执行测量操做么?


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

相关文章
相关标签/搜索