请描述View的三大绘制流程(measure、layout、draw)

5 阅读3分钟

在 Android 中,View 的三大绘制流程measure(测量)、layout(布局)和 draw(绘制)。它们共同决定了视图在屏幕上的大小、位置和最终显示效果。这三个过程遵循 “父视图驱动子视图” 的机制,从 ViewRootImpl 开始自上而下执行。


1. Measure(测量)—— 确定宽高

目的:计算出 View 的宽度和高度(即 getMeasuredWidth()getMeasuredHeight())。
关键方法measure()onMeasure()

核心逻辑

  • MeasureSpec:父容器对子视图的“测量约束”,由 模式尺寸 组成。
    • UNSPECIFIED:无限制(如 ScrollView 内部)
    • EXACTLY:精确值(如 match_parent 或固定 dp)
    • AT_MOST:最大值(如 wrap_content

执行流程

  1. 父容器根据自身的 MeasureSpec 和子布局参数,为每个子视图生成一个 MeasureSpec。
  2. 调用子视图的 measure(childWidthMeasureSpec, childHeightMeasureSpec)
  3. 子视图在 onMeasure() 中根据传入的 MeasureSpec,计算出自己的宽高,并通过 setMeasuredDimension(width, height) 保存。

注意事项

  • measure 可能被多次调用(如父容器为了协调布局会反复测量)。
  • onMeasure() 中必须调用 setMeasuredDimension(),否则会抛出异常。
  • 自定义 View 时,通常需要重写 onMeasure() 来处理 wrap_content(默认行为与 match_parent 相同)。

2. Layout(布局)—— 确定位置

目的:确定 View 在父容器中的四个顶点坐标(left, top, right, bottom),即最终显示位置。
关键方法layout()onLayout()

执行流程

  1. 父容器在 onLayout() 中遍历所有子 View。
  2. 根据测量出的宽高及自身布局规则(如 LinearLayout 的排列方向),为每个子视图计算位置。
  3. 调用子视图的 layout(left, top, right, bottom)
  4. 子视图保存这些坐标,并在 onLayout() 中(如果是 ViewGroup)继续触发其子视图的 layout。

获取最终位置

  • getLeft() / getTop() / getRight() / getBottom()
  • getWidth() = getRight() - getLeft()(通常等于测量宽度,但不绝对)

与 measure 的区别

流程输入输出
measureMeasureSpec(约束)测量宽/高
layout测量宽/高 + 父容器坐标实际位置 + 最终宽/高

3. Draw(绘制)—— 显示内容

目的:将 View 的内容绘制到屏幕上(背景、文本、图像等)。
关键方法draw()onDraw()

绘制步骤(draw() 内部顺序)

  1. 绘制背景drawBackground()
  2. 自身内容(调用 onDraw()
  3. 子视图dispatchDraw()
  4. 前景 / 滚动条onDrawForeground()

注意事项

  • 避免在 onDraw() 中创建新对象(防止 GC 频繁触发)。
  • 不要主动调用 draw(),需要重绘时使用 invalidate()(UI 线程)或 postInvalidate()(非 UI 线程)。
  • 如果 View 没有背景且内容为空,系统会跳过绘制(但 View 仍会触发子视图绘制)。

整体调用链(从 ViewRootImpl 开始)

ViewRootImpl.performTraversals()
    ├── performMeasure()  → child.measure()
    ├── performLayout()   → child.layout()
    └── performDraw()     → child.draw()
  • 首次进入时三个流程都会执行。
  • 若仅改变位置(如动画平移)且大小不变 → 只执行 layout
  • 若仅改变外观(如文字变色)且大小/位置不变 → 只执行 draw(调用 invalidate()

总结:三个流程的职责

流程做的事关键方法输出
measure量多大onMeasure()测量宽/高
layout放哪onLayout()四顶点坐标
draw画什么onDraw()像素显示

了解这三个流程可以帮助你:

  • 实现自定义 View(必须重写 onMeasure / onDraw,ViewGroup 还需重写 onLayout
  • 排查布局异常(如 wrap_content 失效、位置偏移)
  • 优化性能(避免过度测量、重叠绘制)