View绘制流程

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绘制流程_第1张图片
measureSpec

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且本身不具备绘制功能时,就可以开启这个标志位,进行后续的优化。

你可能感兴趣的:(View绘制流程)