onMeasure():这是什么,MeasureSpec模式及方法重写

作者: IT Sectr 发布日期: 2026-07-22 阅读时间: 9 分钟

onMeasure() — 是android.view.View类的protected方法,由Android系统调用以确定View的尺寸。系统向该方法传递两个MeasureSpec对象,每个对象包含测量模式(EXACTLY、AT_MOST或UNSPECIFIED)和父容器建议的尺寸。根据Android Developers Documentation(2026),正确处理MeasureSpec的onMeasure重写是所有需要精确控制尺寸的自定义View和ViewGroup的强制步骤。

要点

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — 用于从父级传递MeasureSpec测量尺寸的View方法
  • MeasureSpec — 编码测量模式(UNSPECIFIED、EXACTLY、AT_MOST)和尺寸的32位值
  • setMeasuredDimension(int w, int h) — onMeasure内的强制调用,固定View的最终尺寸
  • 双遍算法测量:父级测量子级,然后子级报告其尺寸,父级做出最终决定
  • measureChildWithMargins — 用于在自定义ViewGroup中测量子View的辅助方法

什么是onMeasure()?

onMeasure(int widthMeasureSpec, int heightMeasureSpec) — 是View类的方法,由Android系统调用以确定视图的宽度和高度。开发人员重写此方法以根据MeasureSpec中传递的约束指定View应具有的尺寸。如果没有正确重写onMeasure,自定义View可能显示不正确或根本不显示。

系统在View生命周期的measure阶段调用onMeasure,该阶段先于layout(onLayout)和draw(onDraw)阶段。如果View没有重写onMeasure,则使用父类的实现,该实现基于background drawable或layout_params设置默认尺寸。调用super.onMeasure(widthMeasureSpec, heightMeasureSpec)仅适用于标准View子类,如TextView或ImageView。

onMeasure的关键要求 — setMeasuredDimension(int, int)调用必须强制存在于方法末尾。如果缺少此调用,系统会生成IllegalStateException异常,提示View未固定测量尺寸。最终尺寸在measure阶段完成后通过getMeasuredWidth()getMeasuredHeight()获取器可用。

MeasureSpec模式:三个关键值

MeasureSpec — 是一个32位整数,其中高2位编码测量模式,低30位编码尺寸。模式决定了View在选择其尺寸时的自由程度。Android提供三种模式:EXACTLY、AT_MOST和UNSPECIFIED。每种模式在onMeasure中规定了不同的处理逻辑。

MeasureSpec模式行为
EXACTLY父级指定了精确尺寸View必须精确适应传递的尺寸,如果不想超出边界
AT_MOST父级设置了最大尺寸View可以选择从0到传递的最大值的任何尺寸
UNSPECIFIED父级不施加限制View可以选择任何期望的尺寸,没有上限限制

从MeasureSpec提取模式和尺寸使用MeasureSpec类的静态方法:MeasureSpec.getMode(int)返回三种模式之一(EXACTLY、AT_MOST、UNSPECIFIED),MeasureSpec.getSize(int)返回以像素为单位的数值尺寸。要创建自己的MeasureSpec,使用MeasureSpec.makeMeasureSpec(int size, int mode)。这三种方法涵盖了onMeasure中处理尺寸的所有场景。

MeasureSpec的典型处理逻辑

标准模式处理MeasureSpec:如果模式是EXACTLY — 使用传递的尺寸作为最终尺寸;如果是AT_MOST — 选择期望尺寸(View内容)和传递的最大值中的最小值;如果是UNSPECIFIED — 使用View的期望尺寸,无限制。这种模式保证了在任何父级约束下的正确行为。

kotlin
override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
    val desiredWidth = 200
    val desiredHeight = 100

    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    val width = when (widthMode) {
        MeasureSpec.EXACTLY -> widthSize
        MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
        else -> desiredWidth
    }
    val height = when (heightMode) {
        MeasureSpec.EXACTLY -> heightSize
        MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
        else -> desiredHeight
    }
    setMeasuredDimension(width, height)
}

使用resolveSize

为简化标准逻辑,Android提供了resolveSizeAndState方法,它接受期望尺寸、MeasureSpec并返回具有正确模式的最终尺寸。此方法在一行代码中实现了上述模式。此外,还提供了resolveSize(int size, int measureSpec)函数,返回不带状态位的纯尺寸。

双遍测量算法

Android使用双遍测量算法(two-pass measure),确保层次结构中的每个View在考虑父级约束和子元素偏好的情况下获得正确的尺寸。在第一遍中,父级向子View传递带有约束的MeasureSpec,子View计算其期望尺寸。在第二遍中,父级就尺寸做出最终决定。

对于ViewGroup,测量过程更复杂:父级必须首先测量其所有子元素,然后根据它们的尺寸确定自己的尺寸。调用measureChildren(int widthMeasureSpec, int heightMeasureSpec)遍历所有子View,并为每个子View调用measure(child, childWidthSpec, childHeightSpec)。测量完所有子元素后,ViewGroup使用自己的尺寸调用setMeasuredDimension

重要细节:measure方法(公共的,final的)不能被重写 — 取而代之的是重写onMeasure。这确保了系统可以在onMeasure调用之前和之后执行服务操作,例如检查尺寸变化和计算后续绘制的脏区域。如果View具有固定尺寸,则可能不需要重写onMeasure。

MEASURED_SIZE_STATE标志

MeasureSpec不仅包括尺寸和模式,还包括状态位(state bits),可通过MeasureSpec.getMode()访问。调用setMeasuredDimension后,状态成为View测量尺寸的一部分,并可通过getMeasuredState()检查。这在ScrollView和其他可滚动容器中用于正确向子级传递约束。

在Kotlin中重写onMeasure的示例

让我们看一个实际示例,创建带有重写onMeasure的自定义View以实现方形显示。SquareView类继承自View,并保证无论传递的MeasureSpec如何,宽度和高度始终相等。在onMeasure中确定较小的边并设置方形尺寸。

kotlin
class SquareView(context: Context)
    : View(context) {

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        val widthSize =
            MeasureSpec.getSize(widthMeasureSpec)
        val heightSize =
            MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(widthSize, heightSize)
        setMeasuredDimension(size, size)
    }
}

带有子元素测量的自定义ViewGroup

ViewGroup需要更复杂的onMeasure逻辑,因为首先测量子元素,然后确定ViewGroup本身的尺寸。CascadeLayout示例将子元素级联排列并带有间距。通过measureChildWithMargins测量所有子元素后,计算总宽度和高度。

kotlin
class CascadeLayout(context: Context)
    : ViewGroup(context) {

    private val cascadeOffset = 40

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        var maxWidth = 0
        var totalHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child,
                widthMeasureSpec,
                cascadeOffset * i,
                heightMeasureSpec, 0)
            maxWidth = maxOf(maxWidth,
                child.measuredWidth +
                cascadeOffset * i)
            totalHeight += child.measuredHeight
        }
        setMeasuredDimension(
            resolveSize(maxWidth, widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec))
    }

    override fun generateLayoutParams(attrs: AttributeSet?)
        : LayoutParams = MarginLayoutParams(context, attrs)

    override fun onLayout(changed: Boolean,
                       l: Int, t: Int,
                       r: Int, b: Int) {
        var top = t
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val left = l + cascadeOffset * i
            child.layout(left, top,
                left + child.measuredWidth,
                top + child.measuredHeight)
            top += child.measuredHeight
        }
    }
}

重写onMeasure时的常见错误

缺少setMeasuredDimension调用 — 最常见的错误。如果开发人员重写了onMeasure但没有调用setMeasuredDimension,应用程序将因IllegalStateException而崩溃。当方法中存在条件分支且其中一个分支缺少调用时,这种情况尤其常见。onMeasure中的每个代码分支都必须以setMeasuredDimension调用结束。

忽略AT_MOST模式 — 第二常见的错误。如果View在AT_MOST模式下始终使用传递的尺寸而不是基于内容计算,父容器将无法正确分配空间。例如,AT_MOST模式下的TextView应计算文本宽度并使用期望宽度和传递宽度中的最小值。忽略AT_MOST会导致即使内容很小,View也会占据所有可用空间。

在onMeasure内部创建对象 — 一个经典的性能错误。由于onMeasure可能被多次调用(每次请求布局时),在此方法内部创建对象(Paint、Rect、String)会污染内存并触发垃圾回收。所有对象应在View的构造函数中创建一次,而在onMeasure中应仅执行尺寸计算逻辑。同样的规则适用于onDraw和onLayout

在ViewGroup中测量子View

measureChildWithMargins — 是ViewGroup的protected方法,它测量一个子View,同时考虑其MarginLayoutParams。该方法接受父级的MeasureSpec以及在宽度和高度上的累积偏移。自动调整子View的MeasureSpec,减去父级的padding和子级的margin,并将调整后的MeasureSpec传递给child.measure()。

对于高级测量逻辑,ViewGroup可以重写measureChild(View child, int parentWidthSpec, int parentHeightSpec)或直接为每个子元素处理MeasureSpec。例如,LinearLayout在onMeasure中遍历所有子View,考虑其layout_weight测量每个子View,并按比例分配剩余空间。这种方法允许实现任意布局算法。

通过测量缓存(measure cache)机制缓存测量结果,可通过某些ViewGroup中的setMeasureWithLargestChildEnabled标志使用。然而,在大多数情况下,onMeasure在每次布局更改时都会重新调用,缓存不适用。在自定义ViewGroup中,建议最小化onMeasure中的计算,而不是依赖缓存。

常见问题解答

自定义View是否必须重写onMeasure?

是的,如果自定义View直接继承自View类。如果继承自具有标准尺寸的TextView、ImageView或Button,则无需重写onMeasure。对于ViewGroup,始终需要重写onMeasure — 否则子元素将无法正确测量。

如果不调用setMeasuredDimension会发生什么?

Android系统抛出IllegalStateException,文本为“The View did not call setMeasuredDimension”。此异常在onMeasure完成后出现在measure()方法中,如果最终尺寸保持为零。如果未通过try-catch处理,异常会导致应用程序崩溃。

getWidth和getMeasuredWidth有什么区别?

getMeasuredWidth()返回在onMeasure中设置的尺寸(测量阶段)。getWidth()返回View在onLayout中考虑了所有位置修正后获得的实际尺寸。对于大多数View,这些值相同,但在自定义ViewGroup中可能不同。

可以在onMeasure中更改动画或View状态吗?

不可以,onMeasure仅用于计算尺寸。在此方法中更改状态、启动动画、使用网络或更新数据违反了Android架构,并可能导致递归的measure调用,因为状态更改可能触发requestLayout。

onMeasure如何与ConstraintLayout交互?

ConstraintLayout基于给定约束独立管理子元素的测量。如果ConstraintLayout内的自定义View重写了onMeasure,它必须正确处理从ConstraintLayout传递的MeasureSpec,否则约束可能不起作用。ConstraintLayout使用带有自己的WidgetContainer的双遍算法进行计算。

总结

  • onMeasure() — 用于确定尺寸的View方法,由Android系统在measure阶段调用
  • MeasureSpec编码测量模式(EXACTLY、AT_MOST、UNSPECIFIED)和从父级传递的尺寸
  • setMeasuredDimension — onMeasure末尾的强制调用,固定最终尺寸
  • resolveSize — 辅助方法,在一行中实现MeasureSpec的标准处理逻辑
  • 双遍算法保证父-子层次结构中的正确测量
  • measureChildWithMargins用于在考虑间距的情况下在自定义ViewGroup中测量子View
  • 强烈不建议在onMeasure内部创建对象,因为存在GC和帧丢失的风险

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读