先把结论放在前面:MeasureSpec 是一个 32 位打包值——高 2 位是模式,低 30 位是尺寸。三种模式的语义只有一句话:EXACTLY 尺寸已定、AT_MOST 尺寸是上限、UNSPECIFIED 没有约束。自定义 View 的 onMeasure 不理解这三种模式,就会在嵌套容器里出现"高度为 0"或"被裁掉"。
MeasureSpec 与测量模式这题检验的是真功夫。这篇围绕MeasureSpec 与测量模式:先把概念立住,再把机制讲透,最后落到项目里怎么用。
解码:mode 与 size 的关系
// 高 2 位 mode,低 30 位 size
private const val MODE_SHIFT = 30
private const val MODE_MASK = 0x3 shl MODE_SHIFT
// 编码(父容器传给子 View 时用)
fun makeMeasureSpec(size: Int, mode: Int): Int =
(size and -0x80000000) or (mode and MODE_MASK)
// 解码
fun getMode(spec: Int): Int = spec and MODE_MASK
fun getSize(spec: Int): Int = spec and -0x80000000
必须讲清的两条反直觉规则:
一、AT_MOST 不是"至少这么大",而是"不能超过这个上限"。 很多人在这里写错。正确处理是:val max = getSize(spec); val want = computeDesired(); setMeasuredDimension(want.coerceAtMost(max), height)。
二、UNSPECIFIED 在滚动容器里会传下来,忽略它就会得到 0。 ScrollView/NestedScrollView 测量子 View 时,高度方向给的正是 UNSPECIFIED——"嵌套滚动容器里自绘控件测量高度为零"这个坑,根因就在这里。
最常见的坑是
嵌套滚动容器里自绘控件测量高度为零,忽略了 UNSPECIFIED 分支。
典型症状:把自定义的折线图 View 放进 NestedScrollView,高度显示为 0 或一个默认值,内容画不出来。原因是 onMeasure 里按 AT_MOST 分支写了兜底,而滚动容器传下来的是 UNSPECIFIED,于是走了另一条默认逻辑算出 0。
完整的三模式处理是这个形态:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val wMode = MeasureSpec.getMode(widthMeasureSpec)
val wSize = MeasureSpec.getSize(widthMeasureSpec)
val hMode = MeasureSpec.getMode(heightMeasureSpec)
val hSize = MeasureSpec.getSize(heightMeasureSpec)
val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
val desiredHeight = when (hMode) {
MeasureSpec.EXACTLY -> hSize // 父容器给了确定高度,照用
MeasureSpec.AT_MOST -> min(desiredHeight, hSize) // 上限约束,取小
MeasureSpec.UNSPECIFIED -> desiredHeight // 没有约束,用自己算的
}
val measuredWidth = resolveSize(desiredWidth, widthMeasureSpec)
setMeasuredDimension(
resolveSize(desiredWidth, widthMeasureSpec),
resolveSize(desiredHeight, heightMeasureSpec)
)
}
这里用 resolveSize 而不是手写 coerceAtMost,因为 View 已经封装好了三模式逻辑。但要讲清它的代价:resolveSize 对 AT_MOST 不会帮你做 EXACTLY 之外的裁剪决策,如果你的 View 有"期望尺寸"与"内部内容需要最小尺寸"两种诉求,仍要自己算。
还有一个更隐蔽的坑:MeasureSpec 与测量模式最好的预防是自动化——把高频踩坑点做成静态检查或单测断言。三个可落地的做法:① 自绘 View 的单测用三种模式各测一次,把 MeasureSpec.makeMeasureSpec(w, mode) 直接构造成参数化测试,断言 measuredWidth/Height 符合预期——这一条能挡掉九成自绘 View 的测量 bug;② 写一个 DebugView 工具在 onMeasure 入口打印收到的 mode 与 size,排查线上布局问题时一眼定位;③ 把"自绘 View 必须处理 UNSPECIFIED"写进代码评审清单。
再补一个实际项目里非常实用的判据:什么时候该用 wrap_content,什么时候该给固定尺寸或 match_constraint。
- 文字类内容(
TextView)用wrap_content正确,但注意多语言下同一份布局可能出现截断,所以关键界面要给maxWidth或用约束 +layout_width=0dp配app:layout_constraintWidth_max。 - 图表类内容(自绘 View)用
wrap_content通常会得到"先用默认尺寸 measure,再按父约束重测"的两轮开销,如果自绘 View 有明确的宽高比(如 1:1 的进度环),直接给固定比例约束更快。 - 嵌套滚动场景里,自绘 View 的高度应当是"内容算出来的确定值",而不是"跟着父容器撑",否则内容变化时高度抖动。
还有一个容易漏的点:onMeasure 里对尺寸取整要一致。 如果你在 onMeasure 里用 ceil() 向上取整、在 onDraw 里用 floor() 画内容,边缘会出现 1px 的缝隙或截断。规范做法是在 onSizeChanged 时把取整后的值存成字段,onDraw 只用这个字段。
三条高频追问
问:measureChildWithMargins 和 measureChild 有什么区别?
measureChild 用 MeasureSpec.makeMeasureSpec(0, UNSPECIFIED) 给子 View 传尺寸,丢掉父容器已有的约束(只保留 padding);measureChildWithMargins 传的是"父容器剩余可用空间减去已用宽度与 margin"作为上限。在 LinearLayout(水平方向)里用错会算出错误的 AT_MOST 上限,导致子 View 被压缩。所以自定义 ViewGroup 里推荐用 measureChildWithMargins,它算出来的上限更贴近真实剩余空间。
问:为什么 onMeasure 里不能直接用 getWidth()?
因为 measure 阶段还没有 layout,getWidth() 返回的是上一次布局的宽度,首次测量时是 0。measure 阶段只能用 getSuggestedMinimumWidth()(minWidth + 背景内边距)、getPaddingLeft/Right 与 MeasureSpec 里的 size。这也是很多新手"measure 出来是 0"的根因。
问:怎样让子 View 忽略父约束完全自己定尺寸?
setMeasuredDimension(自己算的宽, 自己算的高) 写回即可,父容器会尊重你写回的值(前提是没有后续的 measureChild 覆盖)。但要注意:如果父容器是 LinearLayout 并开了 weight,之后还有一轮重测会按 weight 重新分配,所以"完全自定"要配合父容器不干预。
问:MarginLayoutParams 在自绘 ViewGroup 里为什么拿不到?
measureChildWithMargins 里要求 LayoutParams 是 MarginLayoutParams 才能应用 margin,否则忽略。若自绘 ViewGroup 用的是自定义 LayoutParams(不继承 MarginLayoutParams),margin 需要自己在 generateLayoutParams 与 onMeasure 里手动处理。这是自绘布局最常见的"margin 失效"根因。
落到项目里怎么做
一条能直接落地的规则:自绘 View 的 onMeasure 必须完整处理三种模式,且用参数化单测覆盖。 这一条定下来,测量类 bug 在评审阶段就被挡掉。
配套实践三条:① 建一个 BaseMeasuredView,把三模式处理与"期望尺寸计算"抽象成模板方法,子类只需实现 computeDesiredSize();② 自绘 ViewGroup 的 LayoutParams 一律继承 MarginLayoutParams;③ 在 debug 包里给自绘 View 打开 measure 调试开关,绘制测量框与 mode 文字,布局问题当场可视化。
给正在准备面试的你
这题考的是"知不知道三模式的语义与它们从哪来"。推荐这条线:先讲 32 位打包(高 2 位 mode / 低 30 位 size)与编解码 → 讲三模式语义,重点点出 AT_MOST 是上限不是下限、UNSPECIFIED 来自滚动容器 → 由此推出 onMeasure 的完整三分支写法与 resolveSize 的用法 → 补四个追问(measureChildWithMargins 差别、getWidth 为 0、UNSPECIFIED 处理、自定义 LayoutParams 与 margin)→ 落到"参数化单测覆盖三种模式"这条工程实践。能把 mode 的来源(父容器如何构造 spec)讲出来,答案就完整了。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:View-绘制流程:measure、layout、draw-三部曲
下一篇预告:自定义-View-进阶:onDraw-与-Paint-的进阶用法
有任何问题欢迎在评论区留言交流。