问题总结

问题

为什么 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 崩溃,这是系统强制的约束。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容