初识ViewRoot和DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,这个过程可参看源码:
View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程最终才将一个View绘制出来的。其中measure用来测量View的宽和高,layout用来确定View在父容器的放置位置,而draw则负责将View绘制在屏幕上,针对perfromTraversals的大致流程,可用下列流程图来表示:
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三个流程、其中在performMeasure中会调用measure方法,measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程,完成整个View树的遍历。同理,performLayout和performDraw的流程类似,唯一不同的是,performDraw的传递过程是在draw方法中的dispatchDraw来实现的,不过并没有本质区别。
DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout中有上下两部分,上面是标题栏,下面是内容栏。在Activity中我们通过setContentView所设置的布局就是被加到内容栏中的。可通过如下代码得到我们所设置的布局;
DecorView其实是一个FrameLayout,View层的事件都先经过DecorView,然后传递给我们的View。
理解MeasureSpec
MeasureSpec
MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指某种测量模式下的规格大小。下面看一下MeasureSpec内部的一些常量定义:
SpecMode有三类,每一类都有特殊的含义:
UNSPECIFIED
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应于layoutParams中的match_parent和具体的数值这两种模式
AT_MOST
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现,它对应于LayoutParams中的Wrap_content
MeasureSpec和LayoutParams的对应关系
对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定的;对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定的。MeasureSpec一旦确定,onMeasure中就可以确定View的测量宽和高。
普通View的MeasureSpec的创建规则如下表:(表中的parentSize是指父容器中目前可使用的大小)
- 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且其大小遵循其LayoutParams中的大小。
- 当View的宽高是match_parent时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View是最大模式并且其大小不会超过父容器的剩余空间。
- 当View的宽高是wrap_content时,不管父容器的模式是精准还是最大,View的模式总是最大化模式,并且其大小不会超过父容器的剩余空间。
- (UNSPECIFIED模式是系统内部多次Measure的情况,一般来说,我们不需要关注此模式)
View的工作流程
View的工作流程主要是指measure、layout、draw这三大流程。其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。
measure过程
如果是一个View,则通过measure方法就完成了其测量过程;如果是ViewGroup,除了要完成自己的测量过程外,还要遍历调用所有子元素的measure执行测量。
View的measure过程
View的measure过程是由measure方法来完成的,measure方法是个final类型的方法,不能重写,measure方法调用了onMeasure方法,实现如下:
getDefaultSize方法的源码如下:
从getDefaultSize方法的实现来看,View的宽高是由specSize决定的,所以,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身的大小,否则在布局中使用wrap_content就相当于使用match_content。解决方法如下:
ViewGroup的measure过程
|
|
和View不同的是,ViewGroup是一个抽象类,因此它没有重写onMeasure方法,但是它提供了一个叫measureChildren的方法。
measureChild的思想就是取出子元素的LayoutParams,然后通过传递进来的父容器的MeasureSpec和子元素的LayoutParams创建资源速度恶MeasureSpec,接着将MeasureSpec直接传递给子View的measure进行测量。
ViewGroup并没有定义具体的测量过程,是因为不同的ViewGroup子类(如LinearLayout、RelativeLayout)有不同的布局特性,实现细节不一样。
View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽高就是0。
解决的方法有如下:
- Activity/View#onWindowFocusChanged
当Activity的窗口得到焦点和失去焦点的时候都会调用一次,且View已经初始化完毕了。 - view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了 - ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalalayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发生改变时,该接口方法将会被回调。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)
通过手动对View进行measure来得到View的宽高……
layout过程
layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。
layout方法的大致流程如下:
首先会通过setFrame方法来设定View的四个顶点的位置,View的四个顶点的位置一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,即父容器确定子元素的位置,和onMeasure方法类似,通过onLayout方法去调用子元素的layout方法,子元素又通过自己的layout方法确定自己的位置,这样一层层传递下去就完成了整个View树的layout过程。onLayout的具体实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高是形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高稍微早些。因此,我们可以认为View的测量宽高等于最终宽高,但是的确存在某些特殊情况会导致两者不同。
draw过程
View的绘制过程遵循如下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw
- 绘制children dispatchDraw
- 绘制装饰 onDrawScrollBars
View的绘制过程的传递是通过dispatchDraw来实现,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。
自定义View须知
- 让View支持wrap_content
- 如果有必要,让你的View支持平padding
- 尽量不要在View中使用Handler,没必要
- VIew中如果有线程或者动画,需要及时停止,参考View#onDetachedfromWindow
- View带有滑动嵌套时,需要处理好滑动冲突