View的绘制流程

作者: 韩明泽 | 来源:发表于2019-02-15 00:32 被阅读21次

    这是年假最后一篇笔记了,本篇文章的内容主要来自《android开发艺术探索》,在文章的最后有这本书的网上版本。

    项目源码

    目录

    • MeasureSpec

      • SpecMode分类
        • UNSPECIFIED
        • EXACTLY
        • AT_MOST
      • MeasureSpec和LayoutParams对应关系
    • measure过程

      • View的measure过程

    1. MeasureSpec

    MeasureSpec代表的是一个32位的int类型的数值,31 ~ 30为测量模式(SpecMode),29 ~ 0(SpecSize) 为宽高的实际大小。一个完整的MeasureSpec是由SpecMode+SpecSize组合而成,可通过makeMeasureSpec()得到MeasureSpec、通过getMode()得到SpecMode、通过getSize()得到SpecSize。

    //打包生成MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    //解包得到SpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    //解包得到SpecSize
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    
    

    SpecMode分类

    模式 二进制数位 描述
    UNSPECIFIED 00 父容器不对View有任何限制,要多大给多大,一般用于系统内部表示一种测量状态
    EXACTLY 01 表示父控件已经测量出View的大小。View的最终大小就是SpecSize指定的大小;它对应两种模式第一种对应LayoutParams的match_parent,另一种是具体的数值
    AT_MOST 10 父容器指定一个SpecSize,View的大小不能不能超过这个值。对应的是LayoutParams的wrap_content

    MeasureSpec和LayoutParams对应关系

    LayoutParams配合父容器的MeasureSpec用于约束View的大小,他们两个共同作用下 生成最终的View的MeasureSpec,从而确定View的宽高。需要注意的是顶层View和普通View的测量有所不同。DecorView的MeasureSpec是由窗口的尺寸和自身LayoutParams共同作用生成,普通View是由父容器的MeasureSpec和自身LayoutParams共同作用生成。

    顶层view的MeasureSpec生成过程:

    ……
    //获取顶层View的宽高
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ……
    //生成顶层View的MeasureSpec
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
    
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // 精度模式,顶层View的大小就是窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // 最大模式,大小不确定,但顶层View的大小不能超过窗口的大小
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // 精度模式,顶层View的大小为LayoutParams的大小
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
    

    对于普通View的measure,是由ViewGroup的measure发起的。ViewGroup会调用他的measureChild()来测量子View的宽高在该方法内部会调用getChildMeasureSpec()获取View的MeasureSpec。
    下面为measureChild()代码:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //获取子View宽度MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        //获取子View高度MeasureSpec
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
    
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    

    下面为getChildMeasureSpec()代码:

    //子View的大小会受父容器的MeasureSpec、自身的LayoutParams、View的padding以及margin影响。
    public static int getChildMeasureSpec(int spec, int padding, int childDimension){
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
            //子元素可用大小为父容器的尺寸减去padding
            int size = Math.max(0, specSize - padding);
    
            int resultSize = 0;
            int resultMode = 0;
            //校验父容器是那种模式
            switch (specMode) {
            // 父容器为具体精度
            case MeasureSpec.EXACTLY:
                if (childDimension >= 0) {
                    //子控件的宽或高大于0,代表其设置了具体的宽高值
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    //子元素为精度模式,占满父容器的剩余空间
                    resultSize = size;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    //当子控件为WRAP_CONTENT的时候不管父控件是精度模式还是最大
                    //化模式,View的模式总是最大化,并且不会超过父容器的剩余空间
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            // 父容器为最大化模式
            case MeasureSpec.AT_MOST:
                if (childDimension >= 0) {
                    // 子控件设置了具体的值
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    //子view为精度模式
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    // 子元素为最大化模式
                    resultSize = size;
                    resultMode = MeasureSpec.AT_MOST;
                }
                break;
            // 父元素为不受限制模式
            case MeasureSpec.UNSPECIFIED:
                if (childDimension >= 0) {
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                }
                break;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    从上面的代码可以知道,返回View的MeasureSpec大致可以分为一下机制情况:

    • 子View为具体的宽/高,那么View的MeasureSpec都为LayoutParams中大小。
    • 子View为match_parent,父元素为精度模式(EXACTLY),那么View的MeasureSpec也是精准模式他的大小不会超过父容器的剩余空间。
    • 子View为wrap_content,不管父元素是精准模式还是最大化模式(AT_MOST),View的MeasureSpec总是为最大化模式并且大小不超过父容器的剩余空间。
    • 父容器为UNSPECIFIED模式主要用于系统多次Measure的情形,一般我们不需要关心。
    此图来自《android开发艺术探讨》

    2. measure过程

    image

    View的measure过程

    view测量的过程是由measure()方法完成。该方法不能被重写(是final类型方法),在该方法内部调用了onMeasure()方法用于测量View的大小:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //设置view的宽/高
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    从上面的代码中我们可以知道getDefaultSize()为获取view测量后的大小,下面为getDefaultSize()的源码:

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
    
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
    

    getDefaultSize()放回的大小两种,第一种为当specMode为AT_MOST、EXACTLY情况下View的大小为specSize也就是测量后的大小,View的最终大小是在layout阶段确认下来的,不过view的测量大小和最终大小,几乎所有情况下都是相等的。
    第二种情况为specMode为UNSPECIFIED,这种模式一般用于系统内部的测量过程,该模式下View的大小为传入getDefaultSize()方法的第一个参数size,从上面的代码可以知道,Size为getSuggestedMinimumWidth()或getSuggestedMinimumHeight()返回的大小。

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    
    }
    
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
    

    getSuggestedMinimumWidth()和getSuggestedMinimumHeight一样,我们只需要分析一个即可,下面以getSuggestedMinimumWidth()为例:
    返回的大小与有没有设置背景有关,当View没有设置背景,返回的为mMinHeight。该值为android.minWidth指定的值(默认为0),如果View指定了背景,view返回的值为max(mMinWidth, mBackground.getMinimumWidth())。

    public int getMinimumWidth() {
        //获取Drawable的原始高度,如果没有原始高度返回的为-1。如:ShapeDrawable无原始高度,BitmapDrawable有原始高度。
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth > 0 ? intrinsicWidth : 0;
    }
    

    总结:

    一般我们在自定View的时候需要重写onMeasure()方法,因为从上面的图表中我们可以知道,当我们指定的属性为warp_content的时候系统返回的是父容器剩余空间的大小,这样就和指定的match_parent给的大小一致了。下面为解决这个问题的方式:

    private int mWidth = 200;
    private int mHeight = 200;
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, mHeight);
        }
    }
    

    参考

    Android开发艺术探索完结篇——天道酬勤

    自定义View,有这一篇就够了

    相关文章

      网友评论

        本文标题:View的绘制流程

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