4 View的工作原理
主要内容
自定义View、View的底层工作原理,比如View的测量流程、布局流程、绘制流程。
4.1 初识ViewRoot和DecorView
ViewRoot的实现是
ViewRootImpl
类,是连接WindowManager和DecorView的纽带,View的三大流程(mearsure、layout、draw)均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView建立连接。root = new ViewRootImpl(view.getContext(),display); root.setView(view,wparams, panelParentView);
- View的三大流程:
- measure用来测量View的宽高
- layout来确定View在父容器中的位置
- draw负责将View绘制在屏幕上
- View的绘制流程从ViewRoot的
performTraversals
方法开始:- performTraversals会依次调用
performMeasure
、performLayout
和performDraw
三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。 - 其中
performMeasure
中会调用measure
方法,在measure
方法中又会调用onMeasure
方法,在onMeasure
方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。
- performTraversals会依次调用
- Measure完成后,
getMeasuredWidth
/getMeasureHeight
方法来获取View测量后的宽/高。 - Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过
getTop
、getBotton
、getLeft
和getRight
拿到View的四个定点坐标。 - DecorView作为顶级View,其实是一个
FrameLayout
,它包含一个竖直方向的LinearLayout
,这个LinearLayout
分为标题栏和内容栏两个部分。在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是R.android.id.content
,通过ViewGroup content = findViewById(R.android.id.content);
可以得到这个contentView。View层的事件都是先经过DecorView,然后才传递到子View。
4. 理解MeasureSpec
测量过程,系统将View的LayoutParams
根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。
4.2.1 MeasureSpec
- MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(在某个测量模式下的规格大小)。
- SpecMode有三种:
- UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
- EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的
match_parent
和具体数值这两种模式 - AT_MOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应
LayoutParams
中的wrap_content
4.2.2 MeasureSpec和LayoutParams的对应关系
View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。
View的measure过程由ViewGroup传递而来,参考ViewGroup的measureChildWithMargins
方法,通过调用子元素的getChildMeasureSpec
方法来得到子元素的MeasureSpec,再调用子元素的measure
方法。书中根据源码总结出以下View的MeasureSpec创建规则:
稍有Android开发经验的人应该都能感受到这种规则:
- 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
- 当View的宽/高是
match_parent
时,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空间。 - 当View的宽/高是
wrap_content
时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。 - 父容器的UNSPECIFIED模式,一般用于系统内部多次Measure时,表示一种测量的状态,一般来说我们不需要关注此模式。
questions:
UNSPECIFIED模式在系统内部多次Measure情况下被使用,具体是什么时候?表现又是什么?是在View的测量过程中发生的吗?书中没有说清楚,但是作为开发者,的确是没有接触到UNSPECIFED的情况。
4.3 View的工作流程
4.3.1 measure过程
分两种情况:
- View通过
measure
方法就完成了测量过程 - ViewGroup除了完成自己的测量过程还会便利调用所有子View的
measure
方法,而且各个子View还会递归执行这个过程。
1 View的measure过程
直接继承View的自定义控件需要重写onMeasure
方法并设置wrap_content
(即specMode是AT_MOST
模式)时的自身大小,否则在布局中使用wrap_content
相当于使用match_parent
。对于非wrap_content
的情形,我们沿用系统的测量值即可。
2 ViewGroup的measure过程
ViewGroup是一个抽象类,没有重写View的onMeasure
方法,但是它提供了一个measureChildren
方法。这是因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,比如LinearLayout
和RelativeLayout
,因此ViewGroup没办法同一实现onMeasure
方法。
measureChildren方法的流程:
- 取出子View的
LayoutParams
- 通过
getChildMeasureSpec
方法来创建子元素的MeasureSpec
- 将
MeasureSpec
直接传递给View的measure方法来进行测量
文章通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程。
- LinearLayout在布局中如果使用match_parent或者具体数值,测量过程就和View一直,即高度为specSize
- LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度总和,并且不超过它的父容器的剩余空间。
- 当然LinearLayout的的最终高度同时也把竖直方向的padding考虑在内
View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height
方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。同时View的measure过程和Activity的生命周期并不是同步执行,因此无法保证在Activity的onCreate``onStart``onResume
时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:
- Activity/View#onWindowFocusChanged。 onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
- view.post(runnable)。 通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
- ViewTreeObserver。
使用
ViewTreeObserver
的众多回调可以完成这个功能,比如OnGlobalLayoutListener
这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout
方法会被回调。需要注意的是,伴随着View树状态的改变,onGlobalLayout
会被回调多次。 view.measure(int widthMeasureSpec,int heightMeasureSpec)。
- match_parent: 无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
具体的数值(dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); //View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的 int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec);
4.3.2 layout过程
layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在layout
方法中,onLayout
方法又会被调用。
layout
方法确定View本身的位置,源码流程如下:
n
setFrame
确定View的四个顶点位置,即确定了View在父容器中的位置- 调用
onLayout
方法,确定所有子View的位置,View和ViewGroup均没有真正实现onLayout
方法。
文章通过LinearLayout的
onLayout
方法里来分析ViewGroup的onLayout
过程。
- 遍历所有子View并调用
setChildFrame
方法来为子元素指定对应的位置setChildFrame
方法实际上调用了子View的layout
方法,形成了递归
4.3.3 draw过程
View的绘制过程遵循如下几步:
- 绘制背景
drawBackground(canvas)
- 绘制自己
onDraw
- 绘制children
dispatchDraw
遍历所有子View的draw
方法 - 绘制装饰
onDrawScrollBars
tips:: ViewGroup会默认启用
setWillNotDraw
为ture,导致系统不会去执行onDraw
,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭WILL_NOT_DRAW
这个标记位,即调用setWillNotDraw(false);
4.4 自定义View
4.4.1 自定义View的分类
- 继承View
通过
onDraw
方法来实现一些效果,需要自己支持wrap_content
,并且padding也要去进行处理。 - 继承ViewGroup 实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程。
- 继承特定的View子类(如TextView、Button)
扩展某种已有的控件的功能,且不需要自己去管理
wrap_content
和padding。 - 继承特定的ViewGroup子类(如LinearLayout)
4.4.2 自定义View须知
- 直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。指定wrap_content模式下的默认宽/高。
- 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
- 尽量不啊哟在View中使用Handler,因为没必要。View内部提供了post系列的方法,完全可以替代Handler的作用。
- View中有线程和动画,需要在View的onDetachedFromWindow中停止。
- View带有滑动嵌套情形时,需要处理好滑动冲突
4.4.3 自定义View的思想
- 掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等
- 面对新的自定义View时,对其分类并选择合适的实现思路。