问题
为什么 requestLayout () 会触发 Measure 和 Layout,但不一定触发 Draw?
- 1、requestLayout () 的核心目的
requestLayout() 是「布局请求」,它的唯一目标是重新计算 View 的尺寸 / 位置(解决 “View 尺寸 / 位置变化” 的问题),而非 “重新绘制 View 内容”。 - 2、底层执行逻辑(源码角度)
// View.java 中 requestLayout() 核心逻辑
public void requestLayout() {
// 1. 标记:当前View需要重新布局
mLayoutRequested = true;
// 2. 向上传递请求到 ViewRootImpl
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
// ViewRootImpl.java 中处理 requestLayout()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 标记:需要执行 Measure + Layout
mLayoutRequested = true;
// 调度绘制(和 invalidate() 走同一个入口)
scheduleTraversals();
}
}
- requestLayout() 会给 ViewRootImpl 打上 mLayoutRequested = true 的标记;
- 当 performTraversals() 执行时,会判断这个标记:
// performTraversals() 中关键判断
if (mLayoutRequested) {
// 执行 Measure + Layout
performMeasure(...);
performLayout(...);
// 执行完后,清除 mLayoutRequested 标记
mLayoutRequested = false;
}
// Draw 的执行条件:是否有脏区域(mDirty 不为空)
if (mDirty.isEmpty()) {
// 没有脏区域 → 不执行 Draw
return;
} else {
performDraw();
}
3、“不一定触发 Draw” 的两种场景
1、 仅尺寸 / 位置变化,View 内容不变
❌ 不触发
requestLayout() 只标记 mLayoutRequested,但没有标记「脏区域」(mDirty 为空),Draw 的执行条件不满足
2、尺寸 / 位置变化 + 内容需要更新
✅ 触发
若同时调用了 invalidate()(标记脏区域),或系统判断 “位置变化导致需要重绘”,mDirty 不为空,会执行 Draw-
通俗举例
比如你把一个 Button 从屏幕左边移到右边:只调用 requestLayout():系统只重新计算 Button 的位置(Layout),但 Button 的文字 / 背景没变,不需要重新绘制,因此不执行 Draw;
调用 requestLayout() + invalidate():系统先重新计算位置,再重新绘制 Button,确保位置和内容都正确显示。
自定义 View 时,为什么重写 onMeasure () 后必须调用 setMeasuredDimension ()?
1. Measure 流程的核心目标
Measure 的最终目的是确定 View 的 mMeasuredWidth 和 mMeasuredHeight(测量宽高),这两个值是后续 Layout 流程的唯一依据(Layout 需要根据 Measure 结果确定 View 位置)。
2. 源码层面的强制约束
// View.java 中 measure() 方法的核心逻辑
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 禁止子类重写 measure(),只能重写 onMeasure()
// 2. 调用 onMeasure(),让子类自定义测量逻辑
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 3. 关键检查:是否调用了 setMeasuredDimension()
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) == 0) {
// 未调用 → 抛出异常,Measure 流程失败
throw new IllegalStateException("onMeasure() did not set the measured dimension by calling setMeasuredDimension()");
}
}
// setMeasuredDimension() 核心逻辑
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 1. 保存测量结果到 mMeasuredWidth/mMeasuredHeight
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// 2. 标记:已设置测量尺寸(PFLAG_MEASURED_DIMENSION_SET)
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
- measure() 是 final 方法,子类无法重写,只能通过 onMeasure() 自定义测量逻辑;
- onMeasure() 执行后,系统会检查 PFLAG_MEASURED_DIMENSION_SET 标记:
- 调用了 setMeasuredDimension() → 标记存在 → Measure 流程正常完成;
- 未调用 → 标记不存在 → 抛出 IllegalStateException 异常,App 崩溃。
3. 不调用的后果
直接崩溃:抛出上述异常,这是最直接的后果;
Layout 流程无法执行:即使没崩溃(比如系统兼容),mMeasuredWidth/mMeasuredHeight 为 0,Layout 阶段会把 View 放在 (0,0) 位置,且宽高为 0,View 完全不可见;
违背 Measure 流程设计:Measure 流程的 “输入” 是 MeasureSpec,“输出” 是测量宽高,setMeasuredDimension() 是输出结果的唯一方式,没有它,Measure 流程就失去了意义。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 解析 MeasureSpec,计算自定义宽高
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 2. 自定义测量逻辑:比如固定宽高为 200dp
int desiredWidth = dp2px(200);
int desiredHeight = dp2px(200);
int measuredWidth = getMeasuredSize(desiredWidth, widthMode, widthSize);
int measuredHeight = getMeasuredSize(desiredHeight, heightMode, heightSize);
// 3. 必须调用:保存测量结果
setMeasuredDimension(measuredWidth, measuredHeight);
}
// 辅助方法:根据 MeasureSpec 计算最终测量尺寸
private int getMeasuredSize(int desired, int mode, int size) {
switch (mode) {
case MeasureSpec.EXACTLY:
return size; // 精确模式,用父容器指定的尺寸
case MeasureSpec.AT_MOST:
return Math.min(desired, size); // 最大模式,取自定义和父容器约束的最小值
case MeasureSpec.UNSPECIFIED:
return desired; // 无约束,用自定义尺寸
default:
return size;
}
}
requestLayout() 只关注 “尺寸 / 位置”,不关注 “内容绘制”,因此只触发Measure+Layout;只有当存在 “脏区域”(需要重绘内容)时,才会触发 Draw;
setMeasuredDimension() 是 Measure 流程的 “结果确认”,负责保存测量宽高,没有它会导致 Measure 失败、App 崩溃,这是系统强制的约束。