onLayout():它是什么、布局算法与方法参数

作者: IT Sectr 发布日期: 2026-07-22 阅读时间: 9 分钟

onLayout() — 是 ViewGroup 类的方法,用于在父容器的坐标平面上确定子 View 的位置和尺寸。Android 系统在测量阶段(onMeasure)之后调用 onLayout,此时每个子 View 的测量宽度和高度已知。根据 Android Developers Documentation (2026),onLayout 是在任何自定义 ViewGroup 中必须重写的方法,因为 ViewGroup 的标准实现不执行子元素的自动定位。

主要观点

  • onLayout(boolean changed, int l, int t, int r, int b) — 用于在父坐标系中确定子元素位置的 ViewGroup 方法
  • child.layout(l, t, r, b) — 对每个子 View 的调用,设置其最终边界
  • Layout 阶段 位于 View 生命周期中 measure 阶段之后、draw 阶段之前
  • getWidth() 和 getHeight() 仅在 onLayout 执行后可用,而 getMeasuredWidth 在 onMeasure 后即可用
  • requestLayout() — 当影响定位的数据变更时,发起重新调用 onMeasure 和 onLayout 的方法

什么是 onLayout()?

onLayout(boolean changed, int l, int t, int r, int b) — 是 ViewGroup 类的 protected 方法,由系统调用以在父容器内定位子 View。开发者在创建具有非标准元素排列(阶梯、网格、棋盘形式或任意坐标)的自定义 ViewGroup 时重写此方法。每个子 View 通过调用 child.layout() 获取其最终边界。

changed 参数指示 ViewGroup 本身的位置或大小自上次 layout 以来是否变化。如果 changed 为 true,所有子元素可能也需要重新定位。l、t、r、b 参数是 ViewGroup 在其父坐标系中的左上角和右下角坐标。在 onLayout 内部,开发者将这些值作为子元素布局的起始坐标。

ViewGroup — 是唯一重写 onLayout 的类。普通 View(非 ViewGroup)没有子元素,不需要 onLayout — 其自身定位由父容器处理。即使普通 View 重写了 onLayout,系统也不会调用它。这是 onLayout 与 onMeasure(为每个 View 调用)之间的根本区别。

Layout 阶段的执行流程

Layout 阶段 从在根 View 上调用公共方法 layout(int l, int t, int r, int b) 开始。该方法设置 View 本身的最终坐标,并在 View 为 ViewGroup 时调用 onLayout。然后 onLayout 递归地为每个子元素调用 child.layout(),过程在层级结构中向下重复。这样,layout 从根部向叶节点传播。

在调用 onLayout 之前,系统检查 View 的尺寸与上一个循环相比是否变化。如果尺寸未变化且未调用 requestLayout,onLayout 可能不会被调用 — 系统使用上一次 layout 的结果。这是一种优化,避免在动画或滑动期间无需要地重新计算位置——此时仅内容变化,尺寸不变。

requestLayout() — 是 View 的方法,告知系统 View 的 layout 已过期,需要重新计算。调用 requestLayout 会导致完整的循环:先调用 onMeasure,再调用 onLayout,然后调用 onDraw。与仅触发重绘的 invalidate 不同,requestLayout 会触发尺寸和位置的完整重新计算。过度调用 requestLayout 是性能问题的常见原因。

onLayout 的参数:l、t、r、b

l (left) — ViewGroup 在父坐标系中左边缘的 X 坐标。t (top) — 上边缘的 Y 坐标。r (right) — 右边缘的 X 坐标。b (bottom) — 下边缘的 Y 坐标。ViewGroup 的宽度计算为 r - l,高度为 b - t。这些坐标已包含 ViewGroup 本身的所有填充(padding)。

onLayout 内部,开发者为每个子 View 调用 child.layout(int childLeft, int childTop, int childRight, int childBottom)。传递给 child.layout 的坐标必须在父 ViewGroup 的坐标系中。通常,childLeft 和 childTop 考虑父容器的 padding 计算:childLeft = l + paddingLeft + offsetXchildTop = t + paddingTop + offsetY

参数描述典型用途
l (left)ViewGroup 在父容器中的左边缘坐标子元素在 X 轴上的起始点
t (top)ViewGroup 在父容器中的上边缘坐标子元素在 Y 轴上的起始点
r (right)ViewGroup 在父容器中的右边缘坐标宽度的上限,r - l = getWidth()
b (bottom)ViewGroup 在父容器中的下边缘坐标高度的上限,b - t = getHeight()

计算子元素的坐标

子元素坐标 按公式计算:childLeft = l + paddingLeft + (marginLeft if present)childRight = childLeft + child.getMeasuredWidth()。垂直方向类似:childTop = t + paddingTop + (marginTop)childBottom = childTop + child.getMeasuredHeight()。计算完这四个值后,调用 child.layout(childLeft, childTop, childRight, childBottom)

Kotlin 中带 onLayout 的自定义 ViewGroup 示例

我们创建一个 FlowLayout — 自定义 ViewGroup,将子 View 按行布局,当当前行满时将元素移到新的行。这相当于在一个平面内带 wrap 的 Flexbox。onLayout 遍历所有子 View,计算每个的位置,并使用正确的边界调用 child.layout()

kotlin
class FlowLayout(context: Context)
    : ViewGroup(context) {

    private val horizontalSpacing = 12
    private val verticalSpacing = 8

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        val parentWidth =
            MeasureSpec.getSize(widthMeasureSpec)
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child,
                widthMeasureSpec, 0,
                heightMeasureSpec, 0)

            if (rowX + child.measuredWidth >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            rowX += child.measuredWidth +
                horizontalSpacing
            maxRowHeight = maxOf(maxRowHeight,
                child.measuredHeight)
        }

        val totalHeight = rowY + maxRowHeight +
            paddingBottom
        setMeasuredDimension(
            resolveSize(parentWidth, widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec))
    }

    override fun onLayout(changed: Boolean,
                       l: Int, t: Int,
                       r: Int, b: Int) {
        val parentWidth = r - l
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val cw = child.measuredWidth
            val ch = child.measuredHeight

            if (rowX + cw >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            child.layout(rowX, rowY,
                rowX + cw, rowY + ch)
            rowX += cw + horizontalSpacing
            maxRowHeight =
                maxOf(maxRowHeight, ch)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?)
        : LayoutParams =
        MarginLayoutParams(context, attrs)
}

onLayout 与 onMeasure 的区别

onMeasureonLayout — 是 View 生命周期中两个连续的阶段,执行根本不同的任务。onMeasure 确定 View 的期望(measured)尺寸,而 onLayout 设置实际(actual)坐标和尺寸。关键区别:onMeasure 中的尺寸可能是临时的,后续由父纠正;而 onLayout 中每个子 View 的最终位置被确定。

onMeasure 为每个 View 调用,包括叶节点类(TextView、ImageView、Button)。onLayout 仅为 ViewGroup 调用。这是因为定位是父容器的责任,而非 View 本身。叶节点 View 通过从父 onLayout 调用的 layout() 获取位置。

getMeasuredWidth()getMeasuredHeight() 在 onMeasure 后可用,而 getWidth()getHeight() 仅在 onLayout 后可用。如果在 onMeasure 内部访问 getWidth(),将返回上一个循环的值或零。因此,在 onMeasure 中计算尺寸时应使用 MeasureSpec 和顺序 children。

使用 onLayout 时的常见错误

未考虑填充的定位 — 实现 onLayout 时的第一个错误。开发者常忘记将父容器的 paddingLeftpaddingTop 添加到子 View 的起始坐标。结果,子元素显示在 ViewGroup 边缘,忽略了通过 setPadding() 或 XML 设置的间距。正确计算:childLeft = paddingLeft + offsetX

为不可见子元素调用 layout — 第二个常见问题。如果 ViewGroup 包含 visibility 为 GONE 的子 View,则无需定位它们——它们不占用空间。但 onLayout 必须正确处理这种情况,跳过 GONE 子元素。对于 INVISIBLE 子元素,layout 仍需要调用——它们保留位置,即使不显示。

忽略 changed 参数 — 第三个错误。changed 参数指示 ViewGroup 的尺寸或位置是否变化。如果 changed == false,可以使用缓存的坐标,无需重新计算所有子元素的 layout。然而,完全缓存 layout 是一项困难的任务,大多数 onLayout 实现每次都简单地重新计算 所有元素。当子元素数量较少时,这是可以接受的。

常见问题

可以不重写 ViewGroup 中的 onLayout 吗?

可以,如果 ViewGroup 使用标准 LayoutParams 且不添加自定义定位逻辑。但 ViewGroup 中 onLayout 的标准实现不执行任何操作——子元素将不会被定位。在实践中,所有 ViewGroup(LinearLayout、RelativeLayout、FrameLayout)都重写了 onLayout。

layout 和 onLayout 的区别是什么?

layout() — 是 View 的公共 final 方法,由系统或父 ViewGroup 调用。它设置 View 本身的坐标,并在 View 为 ViewGroup 时调用 onLayout。onLayout() — 是 protected 方法,由开发者重写以实现子元素的自定义布局。

onLayout 可以调用 requestLayout 吗?

技术上可以 — 是的,可以。但绝不推荐,因为会导致无限递归:requestLayout → onMeasure → onLayout → requestLayout。如果在 onLayout 内调用了 requestLayout,系统将抛出 StackOverflowError 异常。所有尺寸变更应在 onLayout 之前完成。

onLayout 如何与动画一起工作?

Layout 动画 (LayoutTransition) 会截获子 View 位置变更并应用过渡动画。开启 LayoutTransition 时,onLayout 先设置最终位置,然后 LayoutTransition 将从旧位置到新位置的移动动画化。这需要使用正确的最终坐标正确实现 onLayout。

为什么在 invalidate 之后没有调用 onLayout?

invalidate() 仅触发 draw 阶段(重绘),不影响 measure 和 layout。要触发 onLayout,需要调用 requestLayout(),它会启动完整的循环:measure → layout → draw。当尺寸和位置不变时,invalidate 更高效地更新外观。

总结

  • onLayout() — 在测量阶段完成后确定子 View 最终位置的 ViewGroup 方法
  • child.layout(l, t, r, b) — 为每个子元素设置坐标的主要机制
  • l、t、r、b 参数 — ViewGroup 在父系统中的边界坐标,宽 = r - l,高 = b - t
  • Layout 阶段 从根 View 向子元素递归传播,在每个 ViewGroup 上调用 onLayout
  • requestLayout() 触发尺寸和位置的完整重新计算循环,而 invalidate 仅触发重绘
  • 考虑 padding 在 onLayout 中是必须的——子元素的起始坐标必须包含父容器的 paddingLeft 和 paddingTop

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读