Android 布局多次测量
Android 传统布局流程
2.1 基本流程
Android 传统 View 系统的布局流程分为两个阶段:
阶段一:测量(Measure)
kotlin
// ViewGroup.onMeasure() 示例
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 1. 解析父容器传递的测量规格
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
// 2. 测量所有子 View
for (i in 0 until childCount) {
val child = getChildAt(i)
val childParams = child.layoutParams
// 3. 根据子 View 的 LayoutParams 计算测量规格
val childWidthSpec = getChildMeasureSpec(
widthMeasureSpec, paddingLeft + paddingRight,
childParams.width
)
val childHeightSpec = getChildMeasureSpec(
heightMeasureSpec, paddingTop + paddingBottom,
childParams.height
)
// 4. 调用子 View 的 measure() 方法
child.measure(childWidthSpec, childHeightSpec)
}
// 5. 根据子 View 的测量结果计算自己的大小
val totalWidth = calculateTotalWidth()
val totalHeight = calculateTotalHeight()
// 6. 调用 setMeasuredDimension() 设置自己的测量大小
setMeasuredDimension(totalWidth, totalHeight)
}
阶段二:布局(Layout)
kotlin
// ViewGroup.onLayout() 示例
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 1. 遍历所有子 View
for (i in 0 until childCount) {
val child = getChildAt(i)
// 2. 计算子 View 的位置
val childLeft = calculateChildLeft(i)
val childTop = calculateChildTop(i)
val childRight = childLeft + child.measuredWidth
val childBottom = childTop + child.measuredHeight
// 3. 调用子 View 的 layout() 方法
child.layout(childLeft, childTop, childRight, childBottom)
}
}
2.2 多次测量问题
在 Android 传统布局系统中,多次测量是常见且必要的情况:
场景一:WRAP_CONTENT 的复杂布局
kotlin
class ComplexLayout(context: Context) : ViewGroup(context) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var totalHeight = 0
var maxWidth = 0
// 第一次测量:获取子 View 的理想大小
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
)
totalHeight += child.measuredHeight
maxWidth = max(maxWidth, child.measuredWidth)
}
// 如果宽度受限,需要重新测量
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST && maxWidth > MeasureSpec.getSize(widthMeasureSpec)) {
// 第二次测量:使用受限宽度重新测量
totalHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(
MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(widthMeasureSpec),
MeasureSpec.EXACTLY
),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
)
totalHeight += child.measuredHeight
}
}
setMeasuredDimension(maxWidth, totalHeight)
}
}
场景二:依赖关系的布局
kotlin
class DependentLayout(context: Context) : ViewGroup(context) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val child1 = getChildAt(0)
val child2 = getChildAt(1)
// 第一次测量:测量 child1
child1.measure(widthMeasureSpec, heightMeasureSpec)
// child2 的大小依赖于 child1 的测量结果
val child2Width = child1.measuredWidth / 2
val child2Height = child1.measuredHeight
// 第二次测量:使用 child1 的结果测量 child2
child2.measure(
MeasureSpec.makeMeasureSpec(child2Width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(child2Height, MeasureSpec.EXACTLY)
)
// 如果 child2 的实际大小影响 child1,可能需要第三次测量
if (child2.measuredHeight != child2Height) {
// 第三次测量:重新测量 child1
child1.measure(widthMeasureSpec, heightMeasureSpec)
}
setMeasuredDimension(
max(child1.measuredWidth, child2.measuredWidth),
child1.measuredHeight + child2.measuredHeight
)
}
}
2.3 多次测量的原因
- 信息不足:父 View 在第一次测量时可能不知道子 View 的理想大小
- 依赖关系:某些子 View 的大小依赖于其他子 View 的测量结果
- 约束冲突:WRAP_CONTENT 和固定大小之间的冲突需要多次协商
- 布局算法限制:某些复杂布局算法(如 ConstraintLayout)需要多次迭代才能收敛