MeasureSpec 的组成和含义

337 阅读8分钟

widthMeasureSpecheightMeasureSpec 是在 Android 自定义 View 中用于衡量 View 尺寸的两个参数。它们并不是直接代表 View 最终的宽高数值,而是由父布局(parent)传入,用于约束子 View 的测量过程的整合参数。理解这两个参数需要从 MeasureSpec 的构成与逻辑出发。

MeasureSpec 的组成和含义:
一个 MeasureSpec 是一个 32 位整型数,由两部分组成:

  1. 测量模式(Mode):

    • EXACTLY(精确值): 父布局已经决定子 View 的最终尺寸,子 View 必须严格使用这个尺寸。
    • AT_MOST(最大值): 父布局提供了一个最大可用空间,子 View 大小不能超过这个值,但可以小于或等于这个值。
    • UNSPECIFIED(未指定): 父布局对子 View 的大小没有任何限制,子 View 想多大就多大(通常出现在类似 ScrollView 等不限制内部内容大小的情境中)。

因此,MeasureSpec 包含的信息是父 View 对子 View 尺寸的期望和限制。这也是为什么需要首先判断父View给子View的测量模式,再根据子View向父View申请的空间大小来确定子View的测量规格

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    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);
}
  1. 尺寸值(Size):
    对应于可用的像素大小,当模式是 EXACTLY 或 AT_MOST 时,会给出一个限制用的像素值;当是 UNSPECIFIED 时,这个值一般没有实际约束意义。

这两个参数在 onMeasure 中的作用:

  • 当框架调用 onMeasure() 时,它会向子 View 传入 widthMeasureSpecheightMeasureSpec,用来告诉子 View 父布局允许或要求的宽高信息。
  • 子 View 在 onMeasure() 中应根据这两个参数来计算自身的测量宽高值,然后通过 setMeasuredDimension() 来设置最终测量结果。
  • widthMeasureSpecheightMeasureSpec 并不是简单的宽度或高度值,而是包含了模式(EXACTLY、AT_MOST、UNSPECIFIED)和可用空间大小的综合信息。

示例代码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}
  • getDefaultSize() 是一个帮助方法,它根据传入的 MeasureSpec 和建议的最小宽/高来返回最终的测量值。

  • getSuggestedMinimumWidth()getSuggestedMinimumHeight() 通常是 View 的最小建议尺寸(可能与背景图或默认值相关)。

  • getDefaultSize() 内部逻辑大致是:

    • 如果 MeasureSpec 的模式是 EXACTLY,则直接使用 MeasureSpec 中给定的精确值。
    • 如果是 AT_MOST 或 UNSPECIFIED,则使用传入的最小建议值与 MeasureSpec 中可能提供的最大值进行比较,最终得到一个合理的尺寸值。

总结来说,widthMeasureSpecheightMeasureSpec 是父布局告知子 View 测量阶段所能使用的测量条件(模式和可用大小),子 View 在 onMeasure() 方法内利用这些条件来决定自己的最终测量尺寸。

整个 MeasureSpec 的传递和模式确定是自上而下层层递进的,每一层级的父 View 在对其子 View 进行测量时,都是基于自己从上一层父 View(或系统窗口)所接收到的测量模式和尺寸约束来确定子 View 的测量模式和大小约束的。

整个过程大致如下:

  1. 应用窗口和根视图(DecorView):
    最顶层的测量是从应用窗口的可用绘制区域开始的。WindowManager 会给最顶级的 DecorView 一个明确的大小(一般为 EXACTLY 模式,尺寸为屏幕或窗口大小)。
  2. 根布局(ViewGroup)测量:
    DecorView 在测量其子 View(如 Activity 的布局根节点:LinearLayout、ConstraintLayout 等)时,会依据自身接收到的 MeasureSpec(通常是 EXACTLY)和子 View 的布局参数来决定子 View 的 MeasureSpec
  3. 逐层传递:
    每一层的 ViewGroup 都会在 onMeasure() 中对自己的子 View 调用 measure(),并通过 getChildMeasureSpec() 函数根据自己的 MeasureSpec 和子 View 的 LayoutParams 来生成子 View 的 MeasureSpec。子 View 的 onMeasure() 再依此规则往下继续传递给下层子 View。
  4. 层层递归:
    最终,从顶层的 EXACTLY(比如从屏幕大小)或其他模式开始,一路向下传递。每一层的布局父 View 所采用的测量模式都是根据它本身收到的 MeasureSpec、布局参数及逻辑计算而来。由于是自顶向下的传递,在最低层的 onMeasure() 中看到 MeasureSpec 时,可以追溯回上一级父 View 的决定,而这上一级父 View 的决定又可追溯到再上一级,最终可回溯到最顶层窗口管理给出的初始约束。

总结: 父 View 的测量模式的确定的确是基于其父 View (或最终是窗口管理器提供的尺寸约束)层层向下传递而来。

  • 父View通过widthMeasureSpec告诉子View什么?

    • 测量模式(Mode) :比如EXACTLY(必须是精确大小)、AT_MOST(最大不超过某个值)、UNSPECIFIED(无限制)。
    • 可用的最大尺寸或精确尺寸(Size) :在EXACTLY和AT_MOST模式下,会有一个具体数值作为上限或要求。
  • 子View根据这个MeasureSpec做什么?
    子View在onMeasure()过程中拿到widthMeasureSpec后,根据传入的模式和尺寸限制,再结合自身的内容、getSuggestedMinimumWidth()以及LayoutParams等因素,决定自己的实际测量宽度(measuredWidth)。

  • 最终确定过程是子View决定自己的测量值
    尽管父View传入的widthMeasureSpec中包含了一定的约束和期望(测量模式与最大可用大小),但真正的测量逻辑是在子View内部完成的。子View会根据这些约束和自身需求来调用setMeasuredDimension(),从而确定自己的最终测量尺寸。 widthMeasureSpec(以及heightMeasureSpec)是父View给子View的“测量规则和限制”。子View拿到这些规则后,再根据自身逻辑去得出最终的实际测量尺寸。换言之,父View通过MeasureSpec告诉子View“我期望你怎么被测量”,而子View则根据这期望和实际情况来决定自己的最终尺寸。

子View需要根据父View的大小与父View的测量模式来确定自己的大小?

是的,子View的最终大小是受父View提供的测量约束所影响的。整个过程可以概括为:父View在测量其子View时,会根据自身的大小与其从上层继承而来的MeasureSpec信息,为子View生成特定的widthMeasureSpecheightMeasureSpec。这些MeasureSpec综合了父View的当前可用空间(或明确大小)以及布局模式(EXACTLY, AT_MOST, UNSPECIFIED)等信息。

子View在onMeasure()中根据这两个MeasureSpec,以及自身的内容、getSuggestedMinimumWidth()getSuggestedMinimumHeight()、自身的LayoutParams(如wrap_content、match_parent、固定值)等因素,来决定自己的实际测量宽高。

简单来说,父View以自己的大小和测量模式来约束子View的大小,子View则根据这些约束和自身需求计算出自己的最终测量尺寸。

子View收到这个MeasureSpec后,不需要根据内容或需求改变大小,而是直接采用这个指定的尺寸,那如果子View的布局参数是wrap_content,用不完这么大的空间,子view还需要严格设置这么大的大小吗?

是的,当子View收到的是EXACTLY模式的MeasureSpec时,即使它的布局参数(LayoutParams)是wrap_content,它仍然需要按照父View指定的精确尺寸进行测量并设置自己的测量结果。这是因为在测量流程中:

  1. 布局参数(如wrap_content、match_parent)仅仅是子View对自身尺寸的偏好,并不具有最终决定权。

  2. 真正的决定权在MeasureSpec

    • EXACTLY模式下的MeasureSpec表示父View已经确定了子View的大小,无论子View内容是否填不满这块空间,它都必须使用这个精确的尺寸。
    • 此时子View的wrap_content偏好会被父View的EXACTLY约束所覆盖。子View在onMeasure()里必须调用setMeasuredDimension()来设置和MeasureSpec中的尺寸相同的值,否则会不符合测量规范。

换句话说,当父View明确给出EXACTLY的测量约束时,子View必须无条件服从这个尺寸要求,即使它的内容比这个空间小得多。