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 类的 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 阶段 从在根 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 是性能问题的常见原因。
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 + offsetX,childTop = 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)。
我们创建一个 FlowLayout — 自定义 ViewGroup,将子 View 按行布局,当当前行满时将元素移到新的行。这相当于在一个平面内带 wrap 的 Flexbox。onLayout 遍历所有子 View,计算每个的位置,并使用正确的边界调用 child.layout()。
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)
}
onMeasure 和 onLayout — 是 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 时的第一个错误。开发者常忘记将父容器的 paddingLeft 和 paddingTop 添加到子 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 使用标准 LayoutParams 且不添加自定义定位逻辑。但 ViewGroup 中 onLayout 的标准实现不执行任何操作——子元素将不会被定位。在实践中,所有 ViewGroup(LinearLayout、RelativeLayout、FrameLayout)都重写了 onLayout。
layout() — 是 View 的公共 final 方法,由系统或父 ViewGroup 调用。它设置 View 本身的坐标,并在 View 为 ViewGroup 时调用 onLayout。onLayout() — 是 protected 方法,由开发者重写以实现子元素的自定义布局。
技术上可以 — 是的,可以。但绝不推荐,因为会导致无限递归:requestLayout → onMeasure → onLayout → requestLayout。如果在 onLayout 内调用了 requestLayout,系统将抛出 StackOverflowError 异常。所有尺寸变更应在 onLayout 之前完成。
Layout 动画 (LayoutTransition) 会截获子 View 位置变更并应用过渡动画。开启 LayoutTransition 时,onLayout 先设置最终位置,然后 LayoutTransition 将从旧位置到新位置的移动动画化。这需要使用正确的最终坐标正确实现 onLayout。
invalidate() 仅触发 draw 阶段(重绘),不影响 measure 和 layout。要触发 onLayout,需要调用 requestLayout(),它会启动完整的循环:measure → layout → draw。当尺寸和位置不变时,invalidate 更高效地更新外观。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。