理解 measure() → onMeasure() → layout() 的完整链路,以及为什么需要两次测量
| 流程 | 作用 | 递归入口 |
|---|---|---|
| 测量流程(Measure) | 从根 View 递归调用 measure(),确定每个 View 想要的尺寸(宽/高) |
ViewRootImpl.performMeasure() |
| 布局流程(Layout) | 从根 View 递归调用 layout(),把测量好的位置和尺寸传给子 View,子 View 保存 |
ViewRootImpl.performLayout() |
三大流程按顺序执行,测量必须先于布局(不知道尺寸就无法确定位置),布局必须先于绘制(不知道位置就没法画)。
因为某些情况需要多次测量才能确定最终尺寸。典型场景:
wrap_content)match_parent或 layout_weight)这就形成了循环依赖,必须通过多次测量来解耦。
如果把测量和布局合在一起(一次遍历确定一切),那么遇到上面的循环依赖场景就无法处理——因为你必须在知道父尺寸之前测量子 View,又在知道子尺寸之前确定父尺寸。
分开之后:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<View android:layout_width="match_parent" android:layout_height="80dp"/>
<View android:layout_width="240dp" android:layout_height="80dp"/>
<View android:layout_width="160dp" android:layout_height="80dp"/>
</LinearLayout>
LinearLayout 宽度 = wrap_content(由子 View 决定),但子 View1 宽度 = match_parent(又依赖父 View)。这就是典型的循环依赖。
父 View 宽度未知(wrap_content尚未计算),以临时约束测量各子 View:
LinearLayout 根据子 View 宽度取最大值,确定自己的最终宽度 = 240dp。
父 View 宽度已确定 = 240dp。再次测量子 View1,此时 match_parent可以生效 → 子 View1 宽度 = 240dp。
布局流程使用第二次测量的结果,调用 layout()将位置和尺寸分配给所有子 View。
这时 LinearLayout 先让子 View 自由测量,此时最长的子 View 宽度就是 LinearLayout 的宽度,然后用这个宽度对所有子 View 再量一次,这下所有子 View 都 match_parent了——宽度一致。
| 场景 | 垂直排列(vertical) | 水平排列(horizontal) |
|---|---|---|
| 宽度是 | 次轴(交叉轴) | 主轴 |
| 二次测量 | 触发 forceUniformWidth,所有子 View 宽度统一为最大宽度 | 不触发二次测量 |
| match_parent 行为 | 最终都被拉伸为相同宽度 | 退化为"在剩余空间内 self-wrap",依次挤压 |
wrap_content的 LinearLayout 中使用 match_parent,或者用 ConstraintLayout扁平化布局。
| 模式 | 含义 | 常见场景 | 例子(父容器宽度=1080px) |
|---|---|---|---|
| EXACTLY | 精确尺寸,子 View 必须遵守 | match_parent/ 固定值 |
layout_width="100dp"→ (EXACTLY, 100) |
| AT_MOST | 最大不超过此值,子 View 可以更小 | wrap_content |
layout_width="wrap_content"→ (AT_MOST, 1080) |
| UNSPECIFIED | 无约束,父容器不限制 | ScrollView 内部、系统特殊场景 | ScrollView 内 → (UNSPECIFIED, 0) |
// ViewGroup 中计算子 View MeasureSpec 的核心逻辑(伪代码)
int getChildMeasureSpec(int parentMeasureSpec, int padding, LayoutParams lp) {
int specMode = MeasureSpec.getMode(parentMeasureSpec);
int specSize = MeasureSpec.getSize(parentMeasureSpec);
int size = Math.max(0, specSize - padding); // 剩余可用空间
if (lp.width == LayoutParams.MATCH_PARENT) {
return MeasureSpec.makeMeasureSpec(
specMode == MeasureSpec.EXACTLY ? size : size, specMode);
} else if (lp.width == LayoutParams.WRAP_CONTENT) {
return MeasureSpec.makeMeasureSpec(
specMode == MeasureSpec.EXACTLY || specMode == MeasureSpec.AT_MOST ? size : 0,
MeasureSpec.AT_MOST);
} else {
return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
}
}
这是 Android 设计中的模板方法模式的经典应用。核心目的:把"测量流程的框架"与"具体的测量逻辑"分离。
| 方法 | 角色 | 能否重写 | 职责 |
|---|---|---|---|
measure() |
流程框架 | final,禁止重写 | 缓存检查 → 清除标志 → 调用 onMeasure() → 校验 → 保存 |
onMeasure() |
具体测量 | protected,子类重写 | 根据 MeasureSpec 计算尺寸 → setMeasuredDimension() |
setMeasuredDimension,或者不调用 onMeasure),导致 View 系统行为异常。所以 Android 把它设为 final,子类只能重写 onMeasure()。
// View.java — measure() 是 final,框架保证流程不被破坏
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 检查缓存:布局参数没变、标志位没要求强制重测、规格相同 → 跳过
boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
if (!forceLayout && widthMeasureSpec == mOldWidthMeasureSpec
&& heightMeasureSpec == mOldHeightMeasureSpec) {
return; // 尺寸没变,直接返回,避免重复计算
}
// 2. 清除测量标志,准备重新测量
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
// 3. 调用 onMeasure 让子类实际计算尺寸(这是唯一允许子类介入的点)
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 4. 严格校验:onMeasure 中必须调用 setMeasuredDimension,否则抛异常
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the measured dimension");
}
// 5. 保存此次规格,供下次缓存判断
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
}
// onMeasure 默认实现:根据背景最小值 + MeasureSpec 默认规则
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
measure()是不变的部分(缓存、标志位、异常检查、调用钩子)→ final,禁止修改。onMeasure()是可变的部分(不同 View 的尺寸计算逻辑不同)→ protected,开放重写。开发者只需关心如何计算尺寸,不用重复编写缓存、校验等样板代码。
逐步骤细致说明:
layout_width、layout_height等)onMeasure()中,根据子 View 的 LayoutParams 和自己的可用空间,得出对子 View 的具体尺寸要求(MeasureSpec)onMeasure()中,根据父 View 的要求以及自己的特性算出期望尺寸。如果是 ViewGroup,还会在这里调用每个子 View 的 measure()进行递归测量layout()方法中,将父 View 传进来的实际尺寸和位置保存measure()方法,由 measure()负责调用 onMeasure()完成实际测量,并保证流程规范(缓存、校验、异常处理)。measure(),测量契约不会被破坏。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 错误:直接返回父容器给的尺寸(AT_MOST 模式下等于父容器宽度)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 1. 计算内容区域所需最小尺寸(不含 padding)
val contentWidth = 200 // 你的内容宽度(如文字/圆的直径)
val contentHeight = 200
// 2. 加上 padding(关键!否则内容会贴边)
val totalWidth = contentWidth + paddingLeft + paddingRight
val totalHeight = contentHeight + paddingTop + paddingBottom
// 3. 按父容器约束决定最终尺寸(必须!)
val width = resolveSize(totalWidth, widthMeasureSpec)
val height = resolveSize(totalHeight, heightMeasureSpec)
// 4. 设置测量结果
setMeasuredDimension(width, height)
}
override fun onDraw(canvas: Canvas) {
// 错误:直接从 (0,0) 开始画,忽略 padding
canvas.drawCircle(0f, 0f, 50f, paint)
}
override fun onDraw(canvas: Canvas) {
// 计算可绘制区域(避开 padding)
val drawLeft = paddingLeft.toFloat()
val drawTop = paddingTop.toFloat()
val drawRight = (width - paddingRight).toFloat()
val drawBottom = (height - paddingBottom).toFloat()
// 在可绘制区域内画圆
val centerX = drawLeft + (drawRight - drawLeft) / 2f
val centerY = drawTop + (drawBottom - drawTop) / 2f
val radius = min(drawRight - drawLeft, drawBottom - drawTop) / 2f
canvas.drawCircle(centerX, centerY, radius, paint)
}
onMeasure和 onDraw中手动处理。
| 属性 | 所属 | View 内部是否需要处理 | 举例 |
|---|---|---|---|
| padding | View 自身 | 必须处理 | android:padding="16dp" |
| margin | 父容器约束 | 完全不用管 | android:layout_margin="8dp" |
父容器在测量子 View 时,会自动扣除 margin(通过 getChildMeasureSpec),子 View 的 measuredWidth不包含 margin。