美文网首页
View绘制流程

View绘制流程

作者: 安卓小白之小楼又东风 | 来源:发表于2018-12-26 22:24 被阅读8次

    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);
    
    measureSpecmeasureSpec

    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绘制流程

          本文链接:https://www.haomeiwen.com/subject/nmtllqtx.html