4.1 初识ViewRoot和DecorView
(1). ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot
来完成的。在ActivityThread
中,当Activity对象被创建完毕后,会将DecorView
添加到Window
中,同事会创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView建立关联。
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
(2). View的绘制流程是从ViewRoot的performTraversals
方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽高,layout用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上。
(3). performMeasure
方法中会调用measure
方法,在measure
方法中又会调用onMeasure
方法,在onMeasure中会对所有子元素进行measure过程,这个时候measure流程就从父容器传递到子元素了,这样就完成了依次measure过程,layout和draw的过程类似。
(4).measure过程决定了view 的宽高,在几乎所有情况下这个宽高都等同于view最终的宽高。layout过程决定了view的四个顶点的坐标和view实际的宽高,通过getWidth
和getHeight
方法可以得到最终的宽高。draw过程决定了view的显示。
(5).DecorView其实是一个FrameLayout,其中包含了一个竖直方向的LinearLaytou,上面是标题栏,下面是内容区域(id为android.R.id.content
)。
4.2理解MeasureSpec
(1). MeasrueSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽和高,不一定等于View的最终宽高。
(2). MeasureSpec代表一个32位的int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
(3). SpecMode有三类,每一类都表示特殊的含义:
UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。他对对应LayoutParams
中的match_parent
和具体的数值这两种模式。
AT_MOST:父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同的View的具体实现。它对应于LayoutParams
中的wrap_content
。
(4). MeasureSpec
和LayoutParams
的对应关系
在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。
MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定view的MeasureSpec,从而进一步确定view的宽高。对于DecorView,它的MeasureSpec由窗口的尺寸和其自身的LayoutParams来决定;对于普通的view,它的MeasureSpec由父容器的MeasureSpec和自身的LayoutPrams来共同决定。
(5). 普通view的MeasureSpec的创建规则
left:parentSpecMode down:childLayoutParams | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_ parent | EXACTLY parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSIze | AT_MOST parentSize | UNSPECIFIED 0 |
4.3 view的工作流程
(1). view的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate
、onStart
、onResume
时某个view已经测量完毕了。如果view还没有测量完毕,那么获得的宽高就都是0.这里给出四种方法来解决这个问题:
Activity/View#onWindowFocusChanged方法:onWindowFocusChanged
方法表示view已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。这个方法会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,这个方法都会被调用,如果频繁地进行onResume
和onPause
,那么onWindowFocusChanged也会被频繁调用。
view.post(runnable):通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,view也已经初始化好了。
ViewTreeObserver:使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener
这个接口,当View树的状态发生改变或者View树内部的View可见性发生改变时,onGlobalLayout方法将会被回调。需要注意的是:伴随着View树的状态改变等,onGlobalLayout会被调用多次
view.measure(int widthMeasureSpec, int heightMeasureSpec):通过手动对view进行measure来得到view的宽高,这个要根据view的LayoutParams来处理:
match_parent
:无法measure出具体的宽高;
wrap_content
:如下measure,设置最大值。
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
精确值:例如100px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
(2). 在view的默认实现中,view的测量宽高和最终宽高是相等的,只不过测量宽高形成于measure过程,而最终宽高形成于layout过程。
(3). draw过程大概有以下几步:
1. 绘制背景:background.draw(canvas);
2. 绘制自己:onDraw();
3. 绘制children:dispatchDraw;
4. 绘制装饰:onDrawScrollBars;
(4). View有一个特殊的方法setWillNotDraw:如果一个View不需要绘制任何内容,那么设置这个标记位为true
以后,系统就会进行相应的优化。默认情况下,View没有启用这个优化标记位,但是VieGroup
会默认启用这个优化标记位。这个标记位对应实际开发的意义是:当我们的自定义控件继承于ViewGroup
并且本身布局配绘制功能时,就可以开启这个标记位从而便于系统的后续优化。
4.4 自定义view
(1). 自定义view的四种类型:
继承View重写onDraw方法:采用这种方法需要自己支持wrap_content,并且padding也需要自己处理
继承VieGroup派生特殊的Layout:采用这种方式稍微复杂一些,需要核实地处理ViewGroup的测量、布局两个过程,并同时处理子元素的测量和布局。
继承特定的view
继承特定ViewGroup
(2). 自定义view注意事项:
1. 让View支持wrap_content
2. 如果有必要,让你的View支持padding
3. 尽量不要在view中使用Handler,因为view内部本身就提供了post方法。
4. view中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow。
5. View带有滑动嵌套情形时,需要处理好滑动冲突。
原书的源码,可以更好的理解这个章节。
问题
- 为什么直接继承View的自定义控件需要设置wrap_content?
如果不设置wrap_content,那么自定义view相当于使用match_parent。如果view在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽高等于specSize,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。解决方案:
// mWidth 和 mHeight都是默认的宽高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec , heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST &&heightSpecMode ==MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth, mHeight);
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize );
} else if(heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize , mHeight);
}
}