#View的工作原理
ViewRoot和DecorView
ViewRoot对应于ViewRootImpl,连接WindowManager和DecorView的纽带。
View的绘制流程从ViewRoot的performTraversals方法开始,经过以下三个过程:
- measure
- layout
- draw
理解MeasureSpec
MeasureSpec是一个会影响到View测量过程的参数。在测量View的宽高的过程中,系统会将View的LayoutParams根据父View的规则转换成对应的MeasureSpec,在进行宽高测量。
MeasureSpec
MeasureSpec是一个32位的int值,高两位代表SpecMode,低30位代表SpecSize。即模式+尺寸。Android里将这两个参数打包成了一个int值来避免过都的内存分配,可以通过get方法解包得到mode和size的分别值。源码里主要是一些“位操作”,类似:
|
|
如源码里给出的,SpecMode有三类:
- UNSPECIFIED:父容器不对View有任何限制,一般系统内部使用。
- EXACTLY:父容器已经检测出View的精确大小,由SpecSize指定。
- AT_MOST:父容器指定一个可用大小SpecSize,子View的大小不能超过这个值。
子View Mode会被父View的specMode所影响,在getChildMeasureSpec方法中,给出了这种影响的具体过程,其流程图如下:
子View会根据父View的Spec不同模式,得到不同的结果。
从流程图和表格可以总结出:
- View的MesureSpec由父View的MesureSpec和自身的LayoutParams共同决定;
- 若View指定了大小,则不管父View的MeasureSpec如何,其Spec将总是ECACTLY,而大小为其指定的大小;
- 子View的LayoutParams为Wrap_content时,无论父类为何种模式,子View总是AT_MOST。因此,对于自定义控件来说,当指定view为wrap_content时,需要指定自身的大小,否则子View会在AT_MOST的模式下,最大程度的利用父View的空间。
- getMeasureSpec方法返回的是一个打包后的MesureSpec,子View的Mode将由其前2位确定,而后30位事实上代表了父View的可用大小,子View将参考这一值,但并不是最终子View的大小(事实上,View的最终大小是在layout阶段被确定的,但是一般情况下,View的测量大小和最终大小相等)。
View的工作流程
View的工作流程主要有:measure、layout、draw,即测量,布局和绘制。
View的measure过程
对于View来说,measure过程就是测量自身尺寸的过程;对于ViewGroup来说,measure过程除了测量自身尺寸外,还要递归的去测量所有children的尺寸。
View的measure过程比较简单:
|
|
基本上,所有的onMeasure都要干一件事:计算好自己的宽高,然后调用setMeasuredDimension方法保存。对于自定义的View,我们要自己计算width和height数值。这里就不贴getDefaultSize的代码了,也比较简单,就是根据SpecMode的值,来判断应该使用什么样的size。
ViewGroup的measure过程
ViewGroup的measure过程除了绘制自身外,还要绘制其children。ViewGroup本身是个抽象类,并没有去实现View的onMeasure方法,其通过一个measureChildren的方法对所有的Children进行测量。
|
|
其中调用了measureChild对每个child进行测量:
|
测量过程本质上和View是一致的,外部传入了需要测量的child视图和父View的MeasureSpec,在调用View中getChildMeasureSpec方法创建MeasureSpec,而测量结果传递到View的measure方法中进行测量。接下去就是一个递归遍历的过程。
由于ViewGroup本身是抽象类,没有实现onMeasure方法,因此需要其具体的实现类,来完成这个方法。典型如LinearLayout、RelativeLayout等。事实上,每个ViewGroup的onMeasure方法考虑的东西很多,Android里LinearLayout源码还比较长,值得一看,可以了解下具体的测量过程。
View 的Measure过程和Activity的生命周期方法并不同步,往往在onCreate方法中去获取View的尺寸,得到的值并不是最终View的尺寸大小,为了在Activity启动时获取一个View的尺寸,有四种方法。
(1) Activity/View#onWindowsFocusChanged
当Activity窗口获得焦点时会被调用,并且这个方法表示View已经初始化完毕。
|
|
(2) view.post(Runnable)
|
|
(3) ViewTreeObserver
|
|
layout过程
layout是在Measure结束后的步骤,将用来确定子View的位置。对于ViewGroup来说,layout方法确定本身的位置,然后调用onlayout方法确定所有子view的位置。对于View而言,其layout过程如下:
|
|
layout方法会先使用setFrame来设定View本身的四个顶点位置,在调用onLayout方法去测量子View的位置,而onlayout是一个抽象方法,对于一个view而言,将不会有什么作用,对于一个ViewGroup而言,将会去确定其中所有子view的位置;同样的,在子view内,也会再调用layout方法确定自身和onlayout方法确定子子view,因此通过一层一层的传递,完成整个view树的layout过程。
ViewGroup的一个实现类是LinearLayout,在LinearLayout中,会重写onlayout方法,来完成自身和子View的布局位置确定:
|
|
LinearLayout布局可以选择横向排列或者纵向排列内部的子View,两者实现逻辑类似,看看layoutVertical(l,t,r,b)的一些代码:
|
|
在layoutVertical中,通过Gravity的属性,来判断child的left、right、top等参数如何计算,这里省略贴代码了。在对一个子view计算好四个坐标后,通过setChildFrame函数记录。注意到在setChildFrame后,childTop会加上这个chil自身的高度,这就意味着下一个child的视图位置一定会在当前child下面,实现垂直排列的效果。而在setChildFrame中,实际上也是调用了view的layout方法:
|
|
draw过程
在Measure和layout之后,意味着每一个view在屏幕上的最终大小和位置都被确定了,这时候就通过draw过程将其绘制到屏幕上,其步骤:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制Children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
123456789101112 > /*> * Draw traversal performs several drawing steps which must be executed> * in the appropriate order:> *> * 1. Draw the background> * 2. If necessary, save the canvas' layers to prepare for fading> * 3. Draw view's content> * 4. Draw children> * 5. If necessary, draw the fading edges and restore layers> * 6. Draw decorations (scrollbars for instance)> */>
View有一个特殊的方法setWillNotDraw,它表示如果一个View不需要绘制本身,可以把这个标志位设为true,以便于系统对其进行优化。显然,一个普通的view一般不会去设置这个标志位,但是在某些ViewGroup中,可能本身并不需要经行绘制,那么可以通过这个方法设置从而优化性能。
|
|
自定义View
按照Android开发艺术探索里的分类,有如下四种情况:
- 继承View重写onDraw 方法
- 继承ViewGroup派生
- 继承已有的实体View,如TextView
- 继承已有的实体ViewGroup,如LinearLayout
总结一下就是:自定义View,毫无疑问都需要直接或者间接继承于View,然后根据具体需要实现的功能,来决定是利用现有的View来扩展,还是从底层开始重写。继承的层次越少,自定义空间就越大,同时难度也越高。因此,自定义View时,需要我们找到一种cost最小的方法去实现我们需要的功能。
自定义View时,一些注意事项:
- View需要去支持wrap_content $$ wrap_content对应的MeasureSpec是AT_MOST,如果View不对wrap_content进行处理,会最大限度的利用父view的空间
- View需要去处理padding 和margin $$ 从之前的三大过程来看,padding和margin是参与到了view的绘制计算中的,如果不处理,则这些属性会无效
- View中尽量不使用Handler $$ 因为View本身提供了post方法来发送消息
- View中如果有线程或者动画,需要及时停止 $$ 一般在onDetachedFromWindow中处理,否则可能造成内存泄露
- View如果带有滑动嵌套,需要处理滑动冲突 $$ 有外部拦截发和内部拦截法
重写onDraw方法
|
|
通过自定义了一个CircleView,重写其onDraw方法,来实现画圆。可以看到,onDraw方法中,我对padding属性经行了处理,使得自定义View才能够对xml文件里的padding属性经行支持;另外,在onMeasure方法里,也对wrap_content的默认属性进行了设置。为了使一个自定义View支持我们需要的自定义属性,需要在values目录下创建一个自定义属性是xml文件:
|
|
之后,在CircleView的构造函数里对属性进行解析:
|
|
最后,在xml布局文件中,正常使用即可。需要注意的是,要对命名空间进行声明,类似:
|
|