上一篇博客主要讲了View的事件体系,主要就View参数的相关概念、滑动以及事件分发进行了详细说明,这次主要看下View的工作原理。主要就基本概念及三大绘制流程进行简要分析,文章主要理论知识来自 Android开发艺术探索
1. ViewRoot和DecorView
当Activity调用setContentView方法后会调用PhoneWindow类的setContentView方法,PhoneWindow是Window的实现类,Window类用来描述Activity视图最顶端的窗口显示和行为操作,PhoneWindow的setContentView方法最终会生成一个DecorView对象,DecorView是PhoneWindow的内部类,继承自FrameLayout,所以调用Activity方法的setContentView方法最终会生成一个FrameLayout类型的DecorView组件,该组件将作为整个应用程序的顶层视图。
在DecorView中添加根布局,根布局中包含一个id为content的FrameLayout内容布局,LayoutInflater将Activity的XML内容布局解析成View树形结构,最后添加到id为content的FrameLayout内容布局中,具体关系可以参考下图
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绘制在屏幕上。
performTraversals会依次调用performMeasure、performLayout、performDraw三个方法,这三个方法分别完成View的measure、layout、draw三大流程,在performMeasure中调用measure方法,在measure方法中又调用onMeasure方法,在onMeasure方法中会对所有子元素进行measure过程。这样measure流程就从父容器传到子元素了,接着子元素继续重复父容器的measure过程。如此就完成了整个View树的遍历。performLayout和performDraw过程与performMeasure都是类似的。
Measure过程决定了View的宽高,measure完成以后可以通过getMeasureWidth和getMeasureHeight获取到View测量后的宽高。几乎所有情况下它都等于实际View的宽高。
Layout过程决定了View的四个顶点坐标和实际View的宽高,完成后通过getTop,getBottom,getLeft,getRight拿到View的四个顶点的位置,getWidth和getHeight拿到View的最终宽高。
DecorView内部一般包含两部分,标题栏和内容栏,Activity通过setContentView所设置的布局文件就是被加到内容栏中的,而内容栏的id是content,所以我们可以通过ViewGroupviewGroup = findViewById(android.R.id.content) 得到content,通过content.getChildAt(0)得到我们设置的View。
2. MeasureSpec
MeasureSpec很大程度决定了View的尺寸规格,在测量过程中,系
统会将View的LayoutParams根据父容器施加的规则转换成MeasureSpec,再根据MeasureSpec测量出View的宽高。
MeasureSpec代表一个32位的int值,分为SpecMode(高2位,测量规格)和SpecSize(低30位,规格大小)。
SpecMode分三类,
UNSPECIFIED : 父容器不对View有任何限制,要多大给多大
EXACTLY : 父容器已经测量出View所需要的精确大小,View的最终大小就是specSize所指定的值。对应于LayoutParams中的match_parent和具体的数值。
AT_MOST : 父容器给定了一个可用大小,View的大小不能大于这个值。对应于LayoutParams中的wrap_content。
MeasureSpec和LayoutParams的关系
对于DecorView,它的MeasureSpec由自身的LayoutParams和窗口的尺寸决定。对于普通的View,由父容器的MeasureSpec和自身的LayoutParams决定。
当View采用固定宽高时,不论父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小是LayoutParams中的大小
当View的宽高时match_parent时,如果父容器是EXACTLY模式,那么View也是EXACTLY模式,并且大小是父容器剩余的空间。如果父容器是AT_MOST模式,那么View也是AT_MOST模式,并且大小不会超过父容器的剩余空间。
当View的宽高是wrap_content时,不论父容器是EXACTLY模式还是AT_MOST模式,View都是AT_MOST模式,并且大小不会超过父容器的剩余空间。
3. View的工作流程
Measure过程
如果只是一个原始的View,通过measure方法就可以完成测量,如果是一个ViewGroup除了测量自己还要遍历所有子View的measure方法,并且子View还会递归执行这个过程。
View的measure过程由measure方法来完成,而measure是一个 final方法,子类不可以重写,而measure方法会调用onMeasure方法。
上述方法给View设置了测量方法的测量值,这个值是通过getDefaultSize方法获得的,我们接这个看这个方法。
这个方法里的size是通过getSuggestedMinimumHeight和getSuggestedMinimumWidth获得,这两个方法的原理都一样。
如果View没有设置背景,那么View的高度就是mMinHeight,这个值是由android:minHeight这个属性设置。
如果View有设置背景,那么View的高度就是mMinHeight和背景最小高度之间的较大者。
那么这个背景最小高度又是啥呢,看下源码
返回的是Drawable的原始高度
我们继续看getDefaultSize方法,根据measureSpec得到specMode,如果是UNSPECIFIED模式,就返回getSuggestedMinimumHeight方法的值,如果是EXACTLY或AT_MOST,返回measureSpec中的specSize。
从getDefaultSize的实现来看,View的宽高由specSize决定,所以直接继承自View的自定义控件需要重写onMeasure方法并设置wrap_content的自身大小,否则在布局中使用wrap_content相当于match_parent。因为这种情况的specMode是AT_MOST模式,宽高是specSize,而specSize又是parentSize,也就是父容器可使用的空间大小,所以View的宽高就等于父容器当前剩余空间的大小,也就相当于在布局文件中使用match_parent。那怎么解决这个问题呢
ViewGroup的measure过程
ViewGroup并没有重写View的onMeasure方法,提供了一个measureChildren方法
ViewGroup在进行measure时对每一个子元素进行测量, 通过measureChild这个方法
这个方法取出子元素的LayoutParams,通过getChildMeasureSpec创建子元素的MeasureSpec,然后传递给子元素的measure方法进行测量。
LinearLayout#onMeasure
在LinearLayout的onMeasure方法中,系统会遍历每个子元素,并对每个子元素执行measureChildBeforeLayout,这个方法会调用子元素的measure方法,子元素进入measure过程。并且系统会通过mTotalLength来存储LinearLayout在竖直方向的初步高度。每测量一个子元素mTotalLength就会增加,增加的主要是子元素的高度以及在竖直方向的margin。子元素测量完毕后,LinearLayout会测量自己的大小。对于竖直的LinearLayout,在水平方向的测量遵循View的测量过程,其布局如果是match_parent或者具体的数值,就与View的过程一致,如果是wrap_content,那么它的高度就是所有子元素占用高度的总和。
View的measure过程完成以后通过getMeasureWidth和getMeasureHeight方法就能获取到View的测量宽高。但有时View可能要多次measure才能确定最终的测量宽高。所以一般在onLayout方法中获取View的最终测量宽高。
获取View测量宽高的时机。
1. 在Activity或View中重写onWindowFocusChanged,在该方法中获取View测量宽高。需要注意的是,Activity在获得焦点或失去焦点时都会调用这个方法。所以在重写时需要加一个是否获得焦点的判断。
2. view.post() 把一个Runnable对象添加到消息队列中,等Looper调用时测量,在run方法里测量。
3. 使用ViewTreeObserver接口,当View树状态发生改变或可见性改变时,接口方法会被回调。所以在第一次调用完后就移除监听。保证以后不会再触发。
4. 通过view.measure(),需要View的LayoutParams
match_parent : 无法测量,因为此时View的宽高是parentSize,而此时并不知道parentSize。
wrap_content : 因为specSize是30位,所以就用View能支持的最大值去构造measureSpec。
具体数值xxx px
Layout过程
Layout的作用是ViewGroup来确定子元素的位置,当ViewGroup位置确定后,它在onLayout方法中会遍历所有子元素并调用layout方法,在layout方法中onLayout方法又会被调用。
Layout方法首先会通过setFrame方法设置View四个顶点的位置,接着调用onLayout方法,确定View的位置,View和ViewGroup中并没有实现这个方法,而是在具体的控件中实现的。具体看下LinearLayout的onLayout
这个方法根据调用了layoutVertical和layoutHorizontal,这两个方法是类似的。layoutVertical方法会遍历所有的子元素并调用setChildFrame方法为子元素指定位置,childTop会逐渐变大,这也符合竖直方向LinearLayout的性质,而setChildFrame就是调用子元素的layout方法,确定自己的位置。这样一层一层就完成了整个View树的layout过程。
之前说过一个问题,View的测量宽高和实际的宽高有区别,因为在View的默认实现中,View的测量宽高和实际宽高是一样的,但是测量宽高形成于measure时期,而最终宽高形成于layout时期,两者的赋值时机不同,在layout方法中可以对测量宽高做一些改变,这就使最终宽高不同于测量宽高,虽然这样没有任何意义。另外measure过程会有多次,其测量值可能与最终宽高不同。但最终还是相同的。
draw过程就是将View绘制到屏幕上面,有以下几步
a. 绘制背景(background.draw)
b. 绘制自己(onDraw)
c. 绘制children(dispatchDraw)
d. 绘制装饰(onDrawScrollBars)
这些方法从draw源码就能看出来
View的绘制传递是通过dispatchDraw完成的,dispatchDraw会遍历所有子元素的draw方法,如此draw事件就一层层传递下去,到这View的工作原理就分析完了。