前言
我们在写代码时,经常会有很多疑问:为什么这个组件的显示效果和我想的不一样?我该如何精确地控制它的位置和尺寸?Modifier 链的顺序到底有什么影响?
其实这都和Compose 的测量与布局流程有关,本文将带你深入理解 Modifier.layout()
以及它背后的核心实现 LayoutModifierNode
,帮助你理清 Compose 布局的原理和实际应用方式。
Modifier.layout()
什么是 Modifier.layout()?
Modifier.layout()
是一个修饰符,可以去自定义目标组件的测量和放置过程,从而修改目标组件的尺寸和位置。
kotlin 代码解读复制代码// LayoutModifier.kt
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutElement(measure)
这个函数接收一个 lambda 表达式作为参数,该表达式会在布局过程中被调用,负责测量组件并确定其位置。
方法解析
lambda表达式接收两个参数:
kotlin 代码解读复制代码Modifier.layout { measurable, constraints ->
// 自定义测量和布局
// 返回 MeasureResult
}
-
measurable
:表示当前被修饰的组件,调用它的measure()
方法,就可以确定组件的实际尺寸。 -
constraints
:表示父组件对当前组件的约束条件,定义了组件可用的最大和最小尺寸。
Constraints 主要包含 minWidth
、maxWidth
、minHeight
、maxHeight
四个属性,用于限定组件的可用空间。
lambda表达式需要返回一个MeasureResult
对象,它包含了组件的最终尺寸、对齐线信息,以及通过placeChildren()
方法执行在layout()
函数中定义的放置逻辑。
kotlin 代码解读复制代码// MeasureResult.kt
interface MeasureResult {
val width: Int // 组件的最终宽度
val height: Int // 组件的最终高度
val alignmentLines: MapInt > // 对齐线信息
fun placeChildren() // 放置子组件的逻辑
}
我们通常调用MeasureScope
提供的layout()
函数来创建MeasureResult
实例:
kotlin 代码解读复制代码layout(width, height) { // 最终尺寸
// 在这里放置子组件
placeable.placeRelative(x, y) // 内容的位置偏移
}
基本用法
我们现在来看看layout()
函数最基本的用法,即不影响组件的测量和布局的写法:
kotlin 代码解读复制代码Text(text = "Hello World.", modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints) // 测量Text组件
// 设置最终尺寸
layout(width = placeable.width, height = placeable.height) {
// 设置Text组件的偏移量(无偏移)
placeable.placeRelative(0, 0)
}
})
这段代码和直接写Text(text = "Hello World.")
的效果相同,我们使用了默认的测量或布局逻辑。
示例:自定义间距修饰符
我们现在来用 layout()
实现一个自定义的间距修饰符,功能类似于官方的 Modifier.padding()
。
kotlin 代码解读复制代码/**
* 自定义间距修饰符
* 在组件四周添加指定的间距
*/
fun Modifier.spacing(
all: Dp = 0.dp,
start: Dp = all,
top: Dp = all,
end: Dp = all,
bottom: Dp = all
) = layout { measurable, constraints ->
// 转换 dp 到像素(px)
val startPx = start.roundToPx()
val topPx = top.roundToPx()
val endPx = end.roundToPx()
val bottomPx = bottom.roundToPx()
// 计算水平和垂直间距总和
val horizontalPadding = startPx + endPx
val verticalPadding = topPx + bottomPx
// 修改约束条件,减少内容可用空间
val newConstraints = constraints.copy(
maxWidth = if (constraints.maxWidth != Constraints.Infinity) {
max(constraints.maxWidth - horizontalPadding, 0)
} else Constraints.Infinity,
maxHeight = if (constraints.maxHeight != Constraints.Infinity) {
max(constraints.maxHeight - verticalPadding, 0)
} else Constraints.Infinity,
// 如果原始约束有最小宽度/高度要求,也需要相应调整
minWidth = max(constraints.minWidth - horizontalPadding, 0),
minHeight = max(constraints.minHeight - verticalPadding, 0)
)
// 测量内容,使用修改后的约束条件来测量
val placeable = measurable.measure(newConstraints)
// 计算最终尺寸
val width = placeable.width + horizontalPadding
val height = placeable.height + verticalPadding
// 返回测量结果
layout(width, height) {
// 放置子组件(有偏移)
placeable.placeRelative(startPx, topPx)
}
}
使用示例:
kotlin 代码解读复制代码Column(Modifier.fillMaxWidth()) {
Text(
"标准 padding",
Modifier
.background(Color.Yellow)
.padding(16.dp)
.background(Color.LightGray)
)
Spacer(Modifier.height(8.dp))
Text(
"自定义 spacing",
Modifier
.background(Color.Yellow)
.spacing(top = 24.dp, bottom = 8.dp, start = 16.dp, end = 16.dp)
.background(Color.LightGray)
)
}
示意图:
其中黄色部分都是间距,灰色部分才是组件的实际内容区域。

constraints参数本来是外层组件对当前被修饰的组件的尺寸限制,当我们使用layout()
修饰符后,我们插入了一个LayoutModifierNode
到布局链中,它可以拦截和修改约束条件。这个节点成为了约束传递的中间者,可以根据需要修改约束条件后再传递给被修饰的组件,就像上面的示例一样。
注意所有参数值都是像素(px),而不是 Dp。如果要使用 Dp,需要转换,调用
roundToPx()
函数,比如8.dp.roundToPx()
, 不过使用这个函数要在Density的上下文中。
使用场景
Modifier.layout()
是用来修改组件的尺寸和位置的,它的本质是给组件的位置和尺寸添加装饰效果,不干涉这个组件内部的测量和布局规则。
所以它的使用场景就很明确了,如果你对一个组件的本身很满意,只要对组件的尺寸、位置做一些额外的调整,就可以使用Modifier.layout()
;如果对组件的内部布局不满足,就要去修改内部源码(如果源码不属于你,可以将代码抄一份过来,再进行修改),不能使用Modifier.layout()
了,因为它触及不了那么深的地方。
LayoutModifierNode
LayoutModifierNode
是 Modifier.layout()
背后的核心实现,它是一个接口,它还是Modifier.Element
的子类,专门用于修改布局过程的修饰符节点。它通过实现measure
方法来拦截和修改测量过程。
kotlin 代码解读复制代码interface LayoutModifierNode : DelegatableNode {
// 核心方法,负责修改测量过程
fun measure(
measureScope: MeasureScope,
measurable: Measurable,
constraints: Constraints
): MeasureResult
}
那LayoutModifierNode是怎么影响测量和布局过程的呢?
我们先来看看元素的测量和布局过程。
元素的测量和布局过程
Composable函数,在实际运行时,会生成LayoutNode对象,它会去做实际的测量、布局、绘制、触摸反馈等工作。
测量和布局工作主要通过内部的 remeasure()
和 replace()
函数完成。
我们来看看测量函数 remeasure()
:
kotlin 代码解读复制代码// 位于LayoutNode.kt
fun remeasure(
constraints: Constraints? = layoutDelegate.lastConstraints
): Boolean {
//..
measurePassDelegate.remeasure(constraints)
}
其中measurePassDelegate是专门用于做测量的工具,点进去这个remeasure()
函数:
kotlin 代码解读复制代码// MeasurePassDelegate.kt
fun remeasure(constraints: Constraints): Boolean {
// ..
performMeasure(constraints)
}
代码很长,但是不必管它,关键代码就只有performMeasure(constraints)
,它是真正做测量工作的,点进去:
kotlin 代码解读复制代码fun performMeasure(constraints: Constraints) {
//..
performMeasureBlock
//..
}
performMeasureBlock
lambda表达式,它是实际完成测量工作的,点进去可以看到:
kotlin 代码解读复制代码val performMeasureBlock: () -> Unit = {
outerCoordinator.measure(performMeasureConstraints)
}
再点进去这个measure()
方法,发现竟然来到了Measurable
接口:
kotlin 代码解读复制代码interface Measurable : IntrinsicMeasurable {
fun measure(constraints: Constraints): Placeable
}
说明outerCoordinator
还没有实现measure()
方法,所以要看看创建outerCoordinator
时,传入的实际对象类型,那里面才真正实现了measure()
方法。
回退到performMeasureBlock
lambda表达式中,点击outerCoordinator
到它的定义处:
kotlin 代码解读复制代码class LayoutNodeLayoutDelegate(
private val layoutNode: LayoutNode,
) {
val outerCoordinator: NodeCoordinator
get() = layoutNode.nodes.outerCoordinator
// ..
}
点击 layoutNode.nodes:
kotlin 代码解读复制代码// LayoutNode.kt
internal val nodes = NodeChain(this)
再进入NodeChain:
kotlin 代码解读复制代码internal class NodeChain(val layoutNode: LayoutNode) {
internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
internal var outerCoordinator: NodeCoordinator = innerCoordinator
private set
}
发现outerCoordinator被赋值为innerCoordinator,而innerCoordinator的类型是InnerNodeCoordinator。
我们进入到InnerNodeCoordinator中查看measure()
方法的具体实现:
kotlin 代码解读复制代码override fun measure(constraints: Constraints): Placeable =
performingMeasure(constraints) {
// before rerunning the user's measure block reset previous measuredByParent for children
layoutNode.forEachChild {
it.lookaheadPassDelegate!!.measuredByParent =
LayoutNode.UsageByParent.NotUsed
}
val measureResult = with(layoutNode.measurePolicy) {
measure(
layoutNode.childLookaheadMeasurables,
constraints
)
}
measureResult
}
就是在这完成最终、实际的测量的。
方法中调用了measure()
方法,返回了一个测量结果给measureResult
,用来稍后在布局流程里面用来摆放组件用的。
测量过程:

现在我们知道了测量过程中会干什么:会调用NodeCoordinator中的measure()方法。
在布局过程中会获取测量结果,去应用到实际的组件上。
怎么影响测量和布局过程
那LayoutModifierNode到底是怎么影响的?
它的工作原理都包含在了LayoutNode的modifier
属性里了。
LayoutNode是运行时实际代表Composable函数的UI节点,我们给每一个Composable函数填写的Modifier参数,经过预处理工作(主要是去掉ComposedModifier),最终会成为LayoutNode的modifier
属性,它是一个Modifier链。
我们现在来看看LayoutNode的modifier
属性的set()函数:
kotlin 代码解读复制代码override var modifier: Modifier = Modifier
set(value) {
//..
nodes.updateFrom(value)
//..
}
nodes.updateFrom(value)
会将我们的Modifier链,转换成Modifier.Node双向链表,将Node双向链表交给nodes进行管理。
kotlin 代码解读复制代码NodeChain(..) {
val innerCoordinator: InnerNodeCoordinator
var outerCoordinator: NodeCoordinator
val tail: Modifier.Node
var head: Modifier.Node
}
并且方法中会调用syncCoordinators()
进行同步,为每个 Modifier.Node
关联对应的 NodeCoordinator
辅助对象,这些 NodeCoordinator
形成一个链式结构,在测量过程中,约束条件从外层NodeCoordinator传递到内层,然后测量结果再从内层传回外层,最终确定组件的尺寸和位置。

LayoutModifierNodeCoordinator 是 LayoutModifierNode 对应的 NodeCoordinator 实现,它的 measure 方法就是 LayoutModifierNode 影响测量过程的关键,每个 LayoutModifierNode
都可以修改约束条件
Modifier 链顺序的实际影响
那我们知道了LayoutModifierNode是怎么影响测量和布局过程:在测量过程中修改约束条件。
那么它Modifier 链顺序的实际影响是什么?
比如
kotlin 代码解读复制代码Box(Modifier.size(100.dp).size(200.dp)) // 最终大小 100dp
Box(Modifier.size(200.dp).size(100.dp)) // 最终大小 200dp
为什么是这样?
情况一:第一个 size(100.dp)把父约束(比如可能是无穷大)限制成最大100dp,再传递下去。而第二个 size(200.dp)接收到的约束已经是最大100dp了,再怎么设置200dp也没用,因为不能突破前面传下来的限制。 于是它会在“最大100dp”的约束下测量,最终尺寸就是100dp。
Box的最终大小是100.dp
情况二:外层的size(200.dp)将约束设为最大200dp,然后内层的size(100.dp)将约束进一步限制为最大100dp。内容在100dp的约束下测量,得到100dp的尺寸。然后这个测量结果向外传递,外层的size(200.dp)修饰符接收到这个结果,但它会强制将最终尺寸设置为200dp(因为这是它的固定尺寸设置)。
所以Box的最终大小是200dp。
评论记录:
回复评论: