View的绘制流程
标签(空格分隔): android
初识ViewRoot和DecorView
View的三大流程:View的measure、layout、draw过程
ViewRoot对应于ViewRootImp,在Activity对象完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象和将ViewRootImpl对象和DecorView建立关联。
measure用来测量View的宽高,layout用来确定View在父容器中的放置位置,而draw负责将View绘制在屏幕上。
DecorView是顶级View,上面是标题栏,和下面是内容栏,在Activity中,setContentView所设置的布局文件其实就是加在内容栏之中的,而内容的id是content。DecorView其实是一个FrameLayout,View层都经过DecorView,然后传递给我们的View。
理解MeasureSpec
Mesurespec很大程度决定了一个View的尺寸规格,还会受到父View的影响,因为父容器影响View的MeasureMeasureSpec的创建过程。
MeasureSpec
MeasureSepc代表一个32位int值,高2位是SpecMode,低30位代表SpecSize,SpecMode是测量模式,而SpecSize是指某种模式下的规格大小。
SpecMode和SpecSize也是一个int值,一组SpecMode和SpecSize可以打包为一个MesureSpec,而一个MeasureSpec可以解包为其原始的SpecMode和SpecSize。
SpecMode:
- UNSPECIFIED:父容器不对View有任何限制,要多大就多大。
- EXACTLY:父容器已经检验出View所需要的精准大小,View最终的大小就是SpecSize所指定的值。相当于match_parent.
- AT_MOST:父容器指定大小为SpecSize,View不能超过这个大小。就相当于wrap_content.(LayoutParams)
MeasureSpec和LayoutParams的关系
在View测量时,系统会将LayoutParams在父容器的约束下转化为对应的MeasureSpec,再根据Measure来确定View测量后的宽高。两者共同确定了View的宽高。
- DecorView,其MeasureSpec是由窗体的尺寸和其自身的LayoutParams来共同确定的。
- 普通的View是由父容器的MeasureSpec和本身的LayoutParams共同决定的。
LayoutParams的参数:
- LayouParams.MATCH_PARENT:精确模式,大小是窗口的大小。
- Layout.WRAP.CONTENT:最大模式,大小不定,但是不能超过窗口大小。
- 固定大小:精确模式,大小是LayoutParams中指定的大小。
MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams,和View的margin和padding有关。
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize-padding);
View的工作流程
主要是指measure、layout、draw这三个流程。
measure过程
1、View的measure的过程
view的measure过程是由其measure来完成的,measure是一个final的方法,不能被子类重写。
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
setMeasureDimension(getDefaulatSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
}
public static int getDefaultsize(int size,int measureSpec){
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specMode = MeasureSpec.getSize(measureSpec);
switch(specMode){
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
}
AT_MOST和EXACTLY:
getDefaultSize返回大小就是measureSpec中的specSize,而这个specSize就是测量后的大小,这里多次提到测量后的大小,是因为最终大小是由layout阶段确定的。
UNSPECIFIED:如果View没有设置背景,那么View的宽度就是mMinWidth。而mMinWith对应于android:minWidth这个属性所指定的值,如果不设置就默认为0,如果设置了背景则返回android:minWidth和背景的最小宽度(最小高度)的最大值。
public int getMinimumWidth(){
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth:0;
}
直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content的自身大小,否则在使用wrap_content就相当于使用match_parent.
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.getMode(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);
}
}
View指定一个默认的内部宽高并在wrap_content时设置其宽高。
2、ViewGroup中的measure过程
对于ViewGroup来说,除了完成自己的measure过程,还会遍历去调用所有的子类的measure方法,子元素再递归去执行这个过程。它没有重写View的onMeasure的方法,提供了一个measureChildren。
protected void measureChildren(int widthMeasureSpec,int heightMeasureSpec){
final int size = mChildrenCount;
final View[] children = mChildren;
for(int i = 0 ;i < size; i++){
final View child = children[i];
if((children.mViewFlags & VISIBILITY_MASK) != GONE){
measureChild(child,widthMeasureSpec,heightMeasureSpec);
}
}
}
measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure的方法测量。ViewGroup是抽象类,这是因为不同的ViewGroup子类有不同的布局特性。
一个有趣的问题
在onCreate、onStart、onResume中均无法正确得到某个View的信息,因为无法保证Activity执行生命周期方法时某个View已经测量完毕了,View的measure过程和Activity生命周期方法不是同步的。
解决这个尴尬的问题:
(1)Activity/View#onWindowFocusChanged
onWindowFouusChanged的含义是:View已经初始完毕了,具体来说就是当Activity继续执行和暂停执行时,onWindowFocusChanged均被调用。
public void onWindowFoucusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
}
(2)view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View的初始化已经好了。
(2)ViewTreeObserver
使用ViewTreeObserver的众多回掉都可以完成这一功能。
如OnGlobalLayoutListener
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int height = view.getMeasuredHeight();
int width = view.getMeasuredWidth();
}
});
}
(4)view.measure
- match_parent:无法得到
- wrap_content:
int widthMeasure = View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.AT_MOST);
int heightMeasure = View.MeasureSpec.makeMeasureSpec((1<<30)-1,View.MeasureSpec.AT_MOST);
view.measure(widthMeasure,heightMeasure);
layout过程
Layout的作用是ViewGroup用来确定子元素的位置。
layout的流程:首先通过setFrame方法来设定View的四个顶点的位置,即初始化mLeft、mRight、mTop、mBottom这四个值,View的四个顶点一旦确定,那么View在父容器的位置就确定了,接着调用onLayout方法,它的子元素的位置。
onLayout方法去调用子元素的layout方法,子元素也通过自己的layout方法来确定自己的位置,这样一层一层的传递下去就完成了整个View树的layout过程。
layout方法中会通过setFrame去设置子元素的四个顶点的位置,setChildFrame中的width和height实际上就是子元素的测量宽高。
getWidth和getHeight方法的返回值刚好就是View的测量宽度和高度。
draw过程
(1)绘制背景background.draw
(2) 绘制自己onDraw
(3)绘制children(dispatchDraw)
(4) 绘制装饰(onDrawScrollBars)
view的dra过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有的子元素的draw方法。从setWillNotDraw方法可以看出,如果一个View不需要绘制任何内容时,设置这个标志位为true,系统会进行相应的优化。当我们自定义控件继承ViewGroup且本身不具备绘制功能时,就可以开启这个标志位,进行后续的优化。