View的工作流程
View的工作流程主要指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和四个顶点的位置,而draw则将View绘制到屏幕上。
1 measure过程
measure过程需要分情况来看,如果是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,出来完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。
1. View的measure过程
View的measure过程由其measure方法来完成,measure方法是一个final方法,也就是说子类不能重写它,在measure方法中会去调用View的onMeasure方法,如下所示:
|
|
上述代码很简洁,setMeasuredDimension
方法会设置View的宽高的测量值,因此只需要看getDefaultSize
这个方法:
|
|
可以看出,getDefaultSize
这个方法很简单,我们只需要看AT_MOST和EXACTLY这两种情况。它返回的值其实就是View测量后的大小,这里多次提到测量后的大小是因为View的最终大小是在Layout的时候确定的,但是几乎所有情况下View的测量大小就等于最终大小。
至于UNSPECIFIED这种情况,一般是用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize
的第一个参数size,即getSuggestedMinimumWidth
/getSuggestedMinimumHeight
方法的返回值,看一下它们的源码:
|
|
这里只看getSuggestedMinimumWidth
的实现,getSuggestedMinimumHeight
和它的原理是一样的。如果View没有设置背景,那么View的宽度为mMinWidth,这个mMinWidth对应于android:minWidth
这个属性所设定的值,这个属性如果不指定,那么mMinWidth默认为0;如果View指定了背景,则View的宽度为max(mMinWidth, mBackground.getMinimumWidth())
。上面已经知道了mMinWidth的含义,那么再来看一下mBackground.getMinimumWidth()
是什么,来看Drawable的getMinimumWidth方法:
|
|
可以看出,它返回的就是Drawable的原始宽度,前提是这个Drawable有原始宽度,否则就返回0。举个例子:ShapeDrawable无原始宽高,而BitmapDrawable有原始宽高(图片的尺寸)
那么getSuggestedMinimumWidth方法总结来说就是:如果View没有设置背景,那么返回android:minWidth
这个属性所设定的值,这个值可能为0;如果View设置了背景,则返回android:minWidth
和背景最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的返回值就是View在UNSPECIFIED情况下的测量宽高。
从getDefaultSize方法的实现来看,View的宽高由specSize决定,所以有如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。因为从上述代码中我们可以知道如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,在这种模式下,它的宽高等于specSize,在这种情况下View的宽高就等于父容器当前的剩余空间大小,这种效果和在布局中使用match_parent完全一致。解决这个问题的方法是在onMeasure中给View指定一个默认的内部宽高并在wrap_content时设置这个宽高即可。对于非wrap_content的情况,我们沿用系统的测量值。至于这个默认的内部宽高大小如何指定,这个没有固定的依据,根据需要灵活指定即可。像TextView、ImageView它们的onMeasure均对warp_content做了特殊处理。
2. ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程之外,还会去遍历调用所有子元素的measure方法,各个子元素再去递归去执行这个过程。然而ViewGroup是一个抽象类,因此没有重写View的onMeasure方法,它提供了一个叫measureChildren的方法:
|
|
主要就是遍历了子元素然后如果子元素可见的话,就去调用measureChild方法:
|
|
measureChild就是取出子元素的LayoutParams,然后通过getChildMeasure Spec来穿件子元素的MeasureSpec,接着讲measures直接传递给View的measure方法来进行测量。
ViewGroup是一个抽象类,所以并没有定义测量的具体过程,所以onMeasure方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等。
下面来分析LinearLayout的onMeasure方法来看ViewGroup的measure过程。
LinearLayout的onMeasure方法如下所示:
|
|
上述代码很简单,我们选择一个来看,比如查看竖直布局的LinearLayout的测量过程,即measureVertical方法。源码比较长,因此只看大致逻辑:
|
|
系统会遍历子元素并对每个子元素执行measureChildBeforeLayout
方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度,每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,如下所示:
|
|
当子元素测量完毕之后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout 而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程则和View有所不同。具体来说就是如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素占用的高度总和,但是仍然不能超过它的父容器的剩余空间,最终高度还需要考虑在竖直方向的padding,参考如下:
|
|
View 的measure过程是三大流程中最复杂的一个,measure完成之后,通过getMeasuredWidth/Height方法就可以正确的获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽高,在这种情形下,在onMeasure中拿到的宽/高很有可能是不准确的,一个比较好的习惯是在onLayout方法中去获取View的测量宽高或者最终宽高。
上面已经对View的measure过程进行了详细的分析,现在要在Activity启动的时候,就获取到View的宽高,在onCreate或者onResume里面去获取这个View的宽高是无法正确的到某个View的宽高信息,因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时,某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽高就是0,这里给出4个方法解决这个问题:
(1)Activity/View#onWindowFocusChanged
onWindowFocusChanged 这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到或者失去焦点的时候都会被调用一次。具体来说,当Activity继续执行和暂停执行时,都会被调用,如果频繁的进行onResume和onPause,那么onWindowFocusChanged也会被频繁的调用。代码如下:
|
|
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View已经初始化好了。代码如下:
|
|
(3)ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View 的可见性发生改变时,onGlobalLayout方法将会被回调,因此这是获取View的宽高的一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次,代码如下:
|
|
2 Layout过程
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法又会被调用。layout方法确定View本身的位置,而onLayout法官法则会确定所有子元素的位置,先看View的layout方法如下:
|
|
大致流程如下:首先会通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop和mButton这四个值,View的四个顶点一旦确定,那么View在父容器中的位置也就确定了;接着会调用onLayout方法,这个方法的用途是父容器确定子元素的位置,和onMeasure方法类似,onLayout的具体实现同样和具体布局有关,所以View和ViewGroup都没有实现onLayout方法。接下来看一下LinearLayout的onLayout方法,如下所示:
|
|
LinearLayout中onLayout的实现逻辑和onMeasure类似,这里来看layoutVertical,代码如下所示:
|
|
可以看到此方法会遍历所有子元素并调用setChildFrame方法来为子元素指定对应的位置,其中childTop会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout特性。至于setChildFrame,它仅仅是调用子元素的layout方法,这样父元素在layout方法中完成自己的定位以后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层层传递下去就完成了整个View树的layout过程。setChildFrame方法实现如下所示:
|
|
setChildFrame中的width和height实际上就是子元素的测量宽高,从下面的代码可以看出这一点:
|
|
在View的默认实现过程中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的neasure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高的赋值时机稍微早一些。在日常开发中,我们可以认为View的测量宽高就等于最终宽高,但是的确存在某些特殊情况会导致两者不一致,举例如下:
重写View的layout方法,代码如下:
|
|
上述代码会导致在任何情况下View的最终宽高总是比测量框高大100px,虽然这样会导致View显示不正常并且也没有实际意义,但是证明了测量宽高的确可以不等于最终宽高。另一种情况是在某些情形下,View需要多次measure才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相同。
3 draw过程
draw的过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:
- 绘制背景 backgroun.draw(canvas)
- 绘制自己 onDraw
- 绘制children (dispatchDraw)
- 绘制装饰 (onDrawScrollBars)
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层传递下去。View有一个特殊的方法,setWillNotDraw方法如下所示:
|
|
注释中可以看出,如果一个View不需要绘制任何内容,那么设置这个标记位为true以后,系统会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是ViewGroup会默认启用这个优化标记位。这个标记位对于开发的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就可以开启这个标记位,从而便于系统进行后续的优化。当明确知道一个ViewGroup需要通过onDraw来绘制内容时,需要显式关闭WILL_NOT_DRAW这个标记位。