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) — 是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 — 是一个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:如果模式是EXACTLY — 使用传递的尺寸作为最终尺寸;如果是AT_MOST — 选择期望尺寸(View内容)和传递的最大值中的最小值;如果是UNSPECIFIED — 使用View的期望尺寸,无限制。这种模式保证了在任何父级约束下的正确行为。
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)
}
为简化标准逻辑,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。
MeasureSpec不仅包括尺寸和模式,还包括状态位(state bits),可通过MeasureSpec.getMode()访问。调用setMeasuredDimension后,状态成为View测量尺寸的一部分,并可通过getMeasuredState()检查。这在ScrollView和其他可滚动容器中用于正确向子级传递约束。
让我们看一个实际示例,创建带有重写onMeasure的自定义View以实现方形显示。SquareView类继承自View,并保证无论传递的MeasureSpec如何,宽度和高度始终相等。在onMeasure中确定较小的边并设置方形尺寸。
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需要更复杂的onMeasure逻辑,因为首先测量子元素,然后确定ViewGroup本身的尺寸。CascadeLayout示例将子元素级联排列并带有间距。通过measureChildWithMargins测量所有子元素后,计算总宽度和高度。
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
}
}
}
缺少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。
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直接继承自View类。如果继承自具有标准尺寸的TextView、ImageView或Button,则无需重写onMeasure。对于ViewGroup,始终需要重写onMeasure — 否则子元素将无法正确测量。
Android系统抛出IllegalStateException,文本为“The View did not call setMeasuredDimension”。此异常在onMeasure完成后出现在measure()方法中,如果最终尺寸保持为零。如果未通过try-catch处理,异常会导致应用程序崩溃。
getMeasuredWidth()返回在onMeasure中设置的尺寸(测量阶段)。getWidth()返回View在onLayout中考虑了所有位置修正后获得的实际尺寸。对于大多数View,这些值相同,但在自定义ViewGroup中可能不同。
不可以,onMeasure仅用于计算尺寸。在此方法中更改状态、启动动画、使用网络或更新数据违反了Android架构,并可能导致递归的measure调用,因为状态更改可能触发requestLayout。
ConstraintLayout基于给定约束独立管理子元素的测量。如果ConstraintLayout内的自定义View重写了onMeasure,它必须正确处理从ConstraintLayout传递的MeasureSpec,否则约束可能不起作用。ConstraintLayout使用带有自己的WidgetContainer的双遍算法进行计算。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。