view动画的实现流程原理

之前去百度面试,被问到一些问题,补间动画的实现原理,属性动画的实现原理,两者的区别,为什么源码设计者要将补间动画设计成只改变位置而不改其真实属性值,这些问题如果不看源码根本无法回答,今天有时间来记录一下充实充实自己。我们来整理一下有关view动画的一下问题:
 ①:调用view.startAnimation()后,动画是立即执行吗?
 ②:假如动画持续时间 300ms,当调用了 View.startAniamtion() 之后,又发起了一次界面刷新的操作,那么界面的刷新是在 300ms 之后也就是动画执行完毕之后才执行的,还是在动画执行过程中界面刷新操作就执行了呢?
问题先留着我们先梳理一遍view动画的框架,搞懂view动画到底是怎么做的,再来慢慢思考上述问题
 既然是阅读源码,一定要明确一个目标,我们到底是要找什么,否则源码太多了看着看着就跑偏了,一会我们就忘记当初我们是在干嘛(其实这是句废话,但是很多时候我老犯这个错,就像一栋一百层的大厦,每个房间都有很漂亮的艺术品,有时候逛着逛着就忘了要去目的地),闲言少叙我们开始源码追踪之旅:
 在完全不熟悉源码的情况下要去追踪整体的框架,最优的选择当然是从start的入口开始,开始点击view.startAnimation(Animation)
在这里插入图片描述
代码不多,只有四个函数,一个一个点进去看看,setStartTime
在这里插入图片描述
没有什么重要操作,就是一些成员变量的赋值,再看下个函数setAnimation
在这里插入图片描述
好像也没什么重要操作(对了,这里说的重要操作就是执行动画的逻辑),主要是将当前的Animation实例赋值给view中一个记录动画的成员变量,也就是说将Animation动画实例与view绑定到一起了,再看看下一个函数,invalidateParentCaches
在这里插入图片描述
这个也没什么东西,一些标志位的赋值对于|=,&=,^=这些为运算符不太了解的可以看看我的这篇文章,在文章的底部有详细介绍:|=,&=,^=位运算符详细介绍,再来看看最后一个函数,这是个刷新UI的函数大家肯定时常用到,invalidate函数
在这里插入图片描述
大家都能看到,在view的invalidate函数中其实是调用了invalidateChild函数,而这里的ViewParent,view的父类那当然就是ViewGroup,我们在ViewGroup类中去找找invalidateChild函数看看有什么发现,
在这里插入图片描述
 ViewGroup类的invalidateChild函数主要是一个do{}while()循环,其他代码被我给删了,对咱们研究的问题不影响,第一次进来的时候parent是viewGroup自己,然后它会一层层的寻找父类,while最后的判断是parent是否等于null,那么parent.invalidateChildInParent(location,dirty)这个函数是不是返回null就很重要,这个函数是接口中的方法,就在当前viewGroup类里,Ctrl+F就能找到,视角转到invalidateChildInParent方法里,所以关键是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 这两个是什么时候赋值给 mPrivateFlags,因为只要有两个标志中的一个时,该方法就会返回 mParent,具体赋值的地方还不大清楚,但能确定的是动画执行时,它是满足 if 条件的,也就是这个方法会返回 mParent。
 一个view的parent是ViewGroup,ViewGroup的parent也是ViewGroup,这样一层层寻找父类,一棵view树Root是ViewRootImpl,最后会寻找到ViewRootImpl.invalidateChildInParent上去,至于说为什么view树的Root根是ViewRootImpl,这跟Activity的启动流程或者说是Activity的绘制流程有关,我们都知道Activity中的setContent(View)最后会将这个View添加到DecorView的ContentView–FrameLayout上,也就是说这个DecorView就是界面View的根View,那为什么说ViewRootImpl又是View树体系的Rootview呢?
 这是因为当一个Activity在回调onResume()时,WindowManager会执行addView,在这里创建一个ViewRootImpl,然后将DecorView的MParent设置成ViewRootImpl,接着继续找动画相关的,我们跟到了 ViewRootImpl 的 invalidateChildInParent() 里去了,看看它做了些什么:
在这里插入图片描述
 一路跟到invalidateRectOnScreen函数里,发现了scheduleTraversal(),而scheduleTraversal()函数的作用是将performTraversal封装到一个Runnable中,然后扔到 Choreographer 的待执行队列里,这些待执行的 Runnable 将会在最近的一个 16.6 ms 屏幕刷新信号到来的时候被执行。而 performTraversals() 是 View 的三大操作:测量、布局、绘制的发起者。

view体系中不论哪个view是发生布局请求(requestLayout)还是绘制请求(invalidate)最后都会走到ViewRootImpl的scheduleTraversal()中去,然后当最近一次屏幕刷新信号发生后,通过performTraversals()从DecorView根部局开始一层层递归遍历measure、layout、draw

 从View.startAnimation()方法一路追踪找到这里我们大体上能把它的框架流程弄懂了,下面我们来总结下步骤:
 ①:View.startAnimation(new Animation()),将Animation对象传递给view,在view中用currentAnimation成员变量记录;
 ②:view中的invalidate方法去会寻找parent.invalidateChild,这时parent会指向ViewGroup类
 ③:ViewGroup类中的invalidateChild有一个do{}while{},这个循环会继续寻找parent,层层传递后会找到view树的根布局ViewRootImpl中的invalidateChildInParent(),invalidateChildInParent()中会调用scheduleTraversal();从而实现measure、layout、draw的UI刷新。
到这里我们就能回答一个疑问了,代码调用View.startAnimation()是不是立即执行动画,很显然并不是,startAnimation()后,只是将Animation对象传递给View,并进行了一系列的初始化,准备开始刷新UI界面,但是要等待16ms一次的场屏刷新的信号发出后才开始执行。
&#8195:上述是view动画执行的流程顺序,但这时动画还没有执行,那么动画到底是在哪里执行的,对View的绘制流程熟悉的同学应该知道,measure和layout方法里肯定是没有的,显然它就在ViewRootImpl类中performDraw绘制view的体系中,而view绘制是交给view自己去绘制的(只有view自己知道该绘制成什么样子,例如老师可以规定什么时候作画,主题是什么,给你发画板,画笔,宣纸,颜料等,但是具体的作画是要你自己去完成的)而且我们都知道performDraw递归遍历是从ViewRootImpl一层层从父类(ViewGroup)将draw事件分发给具体某个view的,现在我们将视角转向ViewGroup类中dispatchDraw(Canvas canvas)函数
在这里插入图片描述
老规矩,前前后后很多代码都给删了,太长也不方便看,只关注重点more |= drawChild(canvas,child,drawingTime),这个函数是在view类中
在这里插入图片描述
这里我们看到了动画相关的,getAnimation()然后applyLegacyAnimation(parent, drawingTime, a, scalingRequired)将ViewGroup类中drawChild的参数传递进入,继续跟进去看看这个方法干了什么
在这里插入图片描述
这下确定动画真正开始执行是在什么地方了吧,都看到 onAnimationStart() 了,View.applyLegacyAnimation就是Animation大显神通的舞台,其核心代码主要分三个部分:

1、初始化Animation(仅初始化一次)调用Animation.initialize(width, height, parentWidth, parentHeight),通过View及ParentView的Size来解析Animation中的相关数据;
2、调用Animation.initializeInvalidateRegion(left, top, right, bottom)来设定动画的初始区域,并在fillBefore为true时计算Animation动画进度为0.0f的数据
3、调用getTransformation根据当前绘制事件生成Animation中对应帧的动画数据
4、根据动画数据设定重绘制区域
 ①:若仅为Alpha动画,此时动画区域为View的当前区域,且不会产生变化
 ②:若包含非Alpha动画,此时动画区域需要调用Animation.getInvalidateRegion进行计算,该函数会根据上述生成动画数据Thransformation中的Matrix进行计算,并与之前的动画区域执行unio操作,从而获取动画的完整区域
5、调用ViewGroup.invalidate(int l, int t, int r, int b)设定绘制区域
Animation 的 getTransformation,这个方法是动画的核心,再跟进去看看:
在这里插入图片描述
这个方法里做了几件事:

记录动画第一帧的时间
根据当前时间到动画第一帧的时间这之间的时长和动画应持续的时长来计算动画的进度
把动画进度控制在 0-1 之间,超过 1 的表示动画已经结束,重新赋值为 1 即可
根据插值器来计算动画的实际进度
调用 applyTransformation() 应用动画效果,所以当我们自定义Animation的时候,重写applyTransformation()会拿到动画的每一帧数据
 当View.applyLegacyAnimation()函数执行完后,view此次绘制的动画数据就构建完毕了,之后便回到View.draw(Canvas, ViewGroup, long)应用动画数据对视图进行绘制刷新,其核心代码如下:
在这里插入图片描述

大家能明显看到,动画数据起作用的地方不是renderNode就是canvas画布,根本没设置到view上去 现在能回答另一个问题了,补间动画(view动画)和属性动画最大的区别,view动画只改变了view的显示位置,并没有改变它的真实属性值,所以当view动画完成后点击事件还是留在之前的位置,你点击动画之后的view是没有效果的,

同时我们也能回答那个动画和刷新UI谁先谁后的问题,这一套源码走下来我们能发现,Animation动画和View的重绘(invalidate())是相爱相杀的,同步进行的,或者可以这么说,view动画只是顺手而为,刷新UI才是view关注的根本,而动画和刷新UI的真实执行时刻是靠底层的场屏信号来决定的,在ViewRootImpl中收到这个场屏信号才会开始执行这些步骤

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值