View工作原理 | 理解MeasureSpec和LayoutParams

1,559 阅读3分钟

前言

本篇文章是理解View的测量原理的前置知识,在说View的测量时,我相信很多开发者都会说出重写onMeasure方法,比如下面方法:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

但是这时你有没有想过这个方法中的参数即2个Int值是什么意思,以及是谁调用该方法的。

正文

本篇文章就从这个MeasureSpec值的意义以及如何得到MeasureSpec这2个角度来分析。

MeasureSpec

直接翻译就是测量规格,虽然我们在开发中会自己使用Java代码写布局或者在XML中直接进行布局,但是系统在真正测量以及确定其View大小的函数onMeasue中,参数却是MeasureSpec类型,那么它和普通的Int类型有什么区别呢?

其实在测量过程中,系统会将View的布局参数LayoutParams根据父View容器所施加的规则转换为对应的MeasureSpec,然后根据这个MeasureSpec便可以测量出View的宽高。注意一点,测量宽高不一定等于View的最终宽高。

其实这里就可以想一下为什么要如此设计,我们在XML中写布局的时候,在设置View的大小时就是通过下面2个属性:

android:layout_width="match_parent"
android:layout_height="wrap_content"

然后再加上padding、margin等共同确定该View的大小;这里虽然没啥问题,但是这个中间转换模式太麻烦了,需要开发者手动读取属性,而且读取各种padding、margin值等,不免会引起错误。

所以Android系统就把这个复杂个转换自己给做了,留给开发者的只有一个宽度MeasureSpec和高度MeasureSpec,可以方便开发者。

MeasureSpec是一个32位Int的值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。下面是MeasureSpec的源码,比较简单:

//静态类
public static class MeasureSpec {
    //移位 30位
    private static final int MODE_SHIFT = 30;
    //MODE_MASK的值也就是110000...000即11后面跟30个0
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    //UNSPECIFED模式的值也就是00...000即32个0
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //EXACTLY模式的值也就是01000...000即01后面跟30个0
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //AT_MOST模式的值也就是10000...000即10后面跟30个0
    public static final int AT_MOST     = 2 << MODE_SHIFT;

     //根据最多30位二进制大小的值以及3个MODE创建出一个32位的MeasureSpec的值
     //32位中高2位00 01 10分别表示模式,低30位代表大小
    public static int makeMeasureSpec( int size,
                                      @MeasureSpecMode int mode) {
        //不考虑这种情况
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    //获取模式
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }

    //获取大小
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。

其中SpecMode有3类,每一类都表示特殊的含义,如下所示:

SpecMode含义
UNSPECIFIED表示父容器不对View做任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
EXACTLY表示父容器已经监测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指的值。它对于于LayoutParams中的match_parent和具体的数值这俩种模式
AT_MOST表示父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值,需要看View的具体实现。对应于LayoutParams中的wrap_content。

从上表格可用发现,一个View的宽高MeasureSpec由它父View和自己的LayoutParams共同决定。

MeasureSpec和LayoutParams的对应关系

上面提到在系统中是以MeasureSpec来确定View测量后的宽高,而正常情况下我们会使用LayoutParams来约束View的大小,所以中间这个转换过程也就是将View的LayoutParams在父容器的MeasureSpec作用下,共同产生View的MeasureSpec

LayoutParams

这个类在我们平时用代码来设置布局的时候非常常见,其实它就是用来解析XML中一些属性的,我们来看一下源码:

//这个是ViewGroup中的LayoutParams
public static class LayoutParams {
    //对应于XML中的match_parent、wrap_parent
    public static final int FILL_PARENT = -1;
    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;
    //宽度
    public int width;
    //高度
    public int height;
    //构造函数
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        //解析出XML定义的属性,赋值到宽和高2个属性上
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }
    //构造函数,用于代码创建实例
    public LayoutParams(int width, int height) {
        this.width = width;
        this.height = height;
    }
    //读取XML中的对应属性
    protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
        width = a.getLayoutDimension(widthAttr, "layout_width");
        height = a.getLayoutDimension(heightAttr, "layout_height");
    }
}

这里我们会发现我们在XML中设置的宽高属性就会在这个ViewGroup的LayoutParams给记录起来

既然说起来LayoutParams,我们就来扩展一下子,因为我们平时在代码中设置这个LayoutParams经常会犯的一个错误就是获取到这个View的LayoutParams,它通常不是ViewGroup.LayoutParams,而是其他的,如果不注意就会强转失败,这里多看2个常见子类。

MarginLayoutParams

第一个就是MarginLayoutParams,一般具体具体View的XXX.LayoutParams都是继承这个父类,代码如下:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    //4个方向间距的大小
    public int leftMargin;
    public int topMargin;
    public int rightMargin;
    public int bottomMargin;

    //分别解析XML中的margin、topMargin、leftMargin、bottomMargin和rightMargin属性
    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();

        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);

        //省略

        a.recycle();
    }

   //省略
}

这个不论我们View在啥ViewGroup的里面,在XML中都可以设置其margin,而这些margin的值都会被保存起来。

具体的LayoutParams

第二个就是具体的LayoutParams,比如这里举例LinearLayout.LayoutParams

首先回顾一下,线性布局的布局参数有什么特点,在XML中在线性布局里写新的View,这时你可以设置宽或者高为0dp,然后设置权重,以及设置layout_gravity这些属性,所以这些属性在解析XML时就会保存到相应的布局参数LayoutParams中,线性布局的布局参数代码如下:

//线性布局的LayoutParams
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    //权重属性
    @InspectableProperty(name = "layout_weight")
    public float weight;
    //layout_gravity属性
    @ViewDebug.ExportedProperty(category = "layout", mapping = {
        @ViewDebug.IntToString(from =  -1,                       to = "NONE"),
        @ViewDebug.IntToString(from = Gravity.NO_GRAVITY,        to = "NONE"),
        @ViewDebug.IntToString(from = Gravity.TOP,               to = "TOP"),
        @ViewDebug.IntToString(from = Gravity.BOTTOM,            to = "BOTTOM"),
        @ViewDebug.IntToString(from = Gravity.LEFT,              to = "LEFT"),
        @ViewDebug.IntToString(from = Gravity.RIGHT,             to = "RIGHT"),
        @ViewDebug.IntToString(from = Gravity.START,             to = "START"),
        @ViewDebug.IntToString(from = Gravity.END,               to = "END"),
        @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL,   to = "CENTER_VERTICAL"),
        @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL,     to = "FILL_VERTICAL"),
        @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
        @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL,   to = "FILL_HORIZONTAL"),
        @ViewDebug.IntToString(from = Gravity.CENTER,            to = "CENTER"),
        @ViewDebug.IntToString(from = Gravity.FILL,              to = "FILL")
    })
    @InspectableProperty(
            name = "layout_gravity",
            valueType = InspectableProperty.ValueType.GRAVITY)
    public int gravity = -1;
    //一样从构造函数中获取对应的属性
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a =
                c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

        weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
        gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

        a.recycle();
    }
}

到这里我们就知道了,其实我们在XML布局中的写的各种大小属性,都会被解析为各种LayoutParams实例给保存起来。

转换关系

前面我们知道既然测量的过程需要这个MeasureSpec,而我们平时在开发中在XML里都是使用View的属性,而上面我们可知不论是XML还是代码最终View的宽高等属性都是赋值到了LayoutParams这个类实例中,所以搞清楚MeasureSpec和LayoutParams的转换关系非常重要

正常来说,View的MeasureSpec由它父View的MeasureSpec和自己的LayoutParams来共同得到,但是对于不同的View,其转换关系是有一点差别的,我们挨个来说一下。

DecorView的MeasureSpec

因为DecorView作为顶级View,它没有父View,所以我们来看一下它的MeasureSpec是如何生成的,在ViewRootImpl的measureHierarchy方法中有,代码如下:

//获取decorView的宽高的MeasureSpec
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//开始对DecorView进行测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

看一下这个getRootMeasureSpec方法:

//windowSize就是当前Window的大小
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        //当布局参数是match_parent时,测量模式是EXACTLY
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        /当布局参数是wrap_content时,测量模式是AT_MOST
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        //具体宽高时,对应也就是EXACTLY
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

这里我们就会发现DecorView的Measure的获取非常简单,当DecorView的LayoutParams是match_parent时,测量模式是EXACTLY,值是Window大小;当DecorView的LayoutParams是wrap_content时,测量模式是AT_MOST,值是window大小

View的MeasureSpec

对于View的MeasureSpec的获取稍微不一样,因为它肯定有父View,所以它的MeasureSpec的创造不仅和自己的LayoutParam有关,还和父View的MeasureSpec有关。

在这里我们先不讨论ViewGroup以及View是如何分发这个测量流程的,后面再说,这里有个我们在自定义ViewGroup时常用的方法,它用来测量它下面的子View,代码如下:

//ViewGroup中的代码,用来自定义ViewGroup时遍历子view,然后挨个进行测量
protected void measureChildWithMargins(
        //子View
        View child,
        //ViewGroup的MeasureSpec,即父View的MeasureSpec
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //子View的LayoutParams
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //获取子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);
    //子View进行测量
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这里先不讨论子View如何去测量,只关注在有父View的MeasureSpec和自己的LayoutParams时,它是如何得到自己的MeasureSpec的,代码如下:

//调用的代码
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
        mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                + widthUsed, lp.width);

这里注意一下参数,第一个参数是父View的MeasureSpec,第三个参数是当前View的宽度,而这里的宽度有3种:wrap_content为-2,match_parent为-1,具体值大于等于0,虽然说是宽度,也包含了View的LayoutParams信息。

第二个参数表示间距,其中mPaddingLeft和mPaddingRight很重要,因为这个属性是不会记录在LayoutParams中的,而且它的涵义是内间距,这里它是写在父ViewGroup中的属性值,比如加了这个paddingLeft属性后,其子View不会从原点开始绘制,它所可用的宽度就会变小,所以View在测量其大小时要把padding排除在外。

然后看一下源码实现:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //父View的测量模式
    int specMode = MeasureSpec.getMode(spec);
    //父View的大小
    int specSize = MeasureSpec.getSize(spec);
    //padding是否大于父View的大小了
    int size = Math.max(0, specSize - padding);

    //子View的大小
    int resultSize = 0;
    //子View的测量模式
    int resultMode = 0;
    
    //这里要明白layoutParams中的wrap_content是-2,match_parent是-1,具体值才大于0
    switch (specMode) {
    //父View的测量模式是精确模式
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //子View当前写死了大小,所以测量模式必是精确模式
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //子View和父View一样大,所以测量模式肯定是精确模式
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //子View是包裹内容,其最大值是父View的大小
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    //父View的测量模式是至多模式
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            //子View大小写死,测量模式必须是精确模式
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //和父类一样,也是父类的至多模式
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //这里要稍微注意一下,由于父类最大多少,所以这个View也是至多模式
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // 不分析
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let them have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

由这里代码可以看出,和DecorView不同的是当前View的MeasureSpec的创建和父View的MeasureSpec和自己的LayoutParams有关

普通View的MeasureSpec创建规则

对于DecorView的转换我们一般不会干涉,这里有一个普通View的MeasureSpce创建规则总结:

子View布局\父View ModeEXACTLYAT_MOSTUNSPECIFIED
dp/dx具体值EXACTLY+childSizeEXACTLY+childSizeEXACTLY+childSize
match_parentEXACTLY+parentSizeAT_MOST+parentSizeUNSPECIFIIED+0
wrap_conentAT_MOST+parentSizeAT_MOST+parentSizeUNSPECIFIIED+0

这个规则必须牢记,在后面View的绘制中我们将具体解析。

总结

本篇文章主要是理解MeasureSpec的设计初衷以及其含义,然后就是一个View的MeasureSpec是通过什么规则转换而来。后面文章我们将具体分析如何利用MeasureSpce来进行测量,最终确定View的大小。

笔者水平有限,有错误希望大家评论、指正。