LayoutModifier
作用是修改 Composable 的尺寸和位置偏移
LayoutModifier 只是一个接口,在实际开发中我们会使用 Modifier.layout() 自定义测量和位置偏移处理,先大致了解一下 layout 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 实际的运用,调用 Modifier.layout 来修改测试和位置偏移处理
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutElement(measure)
private data class LayoutElement(
val measure: MeasureScope.(Measurable, Constraints) -> MeasureResult // lambda 函数
) : ModifierNodeElement<LayoutModifierImpl>() {
override fun create() = LayoutModifierImpl(measure)
override fun update(node: LayoutModifierImpl) {
node.measureBlock = measure
}
override fun InspectorInfo.inspectableProperties() {
name = "layout"
properties["measure"] = measure
}
}
// 测量结果
interface MeasureResult {
val width: Int
val height: Int
val alignmentLines: Map<AlignmentLine, Int> // 一般用于 baseLine 的设置
fun placeChildren() // 子组件的测量
}
|
在 Compose 中,如果我们要拦截控件的测量和摆放一般是使用的 Modifier.layout() 这个函数, 可以看到这个 LayoutModifier 最终的实现是 LayoutModifierImpl , 最终在这个 layout 的 lambda 表达式中,需要返回 MeasureResult 对象,而在 Compose 有一个 layout() 函数是 Compose 给我们封装好了的一个 MeasureResult 对象返回,其中 MeasureResult 维护了组件宽高,文字的基准线等等
测量和摆放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Preview
@Composable
fun layoutModifier() {
/** modifier.layout() 可以对当前的控件进行测量,和设置偏移,设置宽高的上限, 但是它仅仅是 针对于当前本控件的,不像传统 View 提供可以对 子view 进行操作 **/
Box(modifier = Modifier
.background(Color.Green)
.layout { measurable, constraints ->
// 测量本控件 (// 可以理解为 传统view 中测量自己大小的步骤 获得自己宽高的尺寸)
val placeable = measurable.measure(constraints)
// 设置当前组件尺寸
layout(placeable.width, placeable.height) {
// 摆放
placeable.placeRelative(0, 0)
}
}) {
Text(text = "Hello World")
}
}
|
上面代码中 layout 第一个参数 measurable 代表是一个可测量对象指被修饰的组件,constraints 代表的是外层组件对被修饰组件的尺寸限制
通过 measurable.measure(constraints) 开始进行测量当前组件,返回的 placeable 中,会包含测量好的宽高等信息,然后通过 layout 函数,将测量好的宽高进行结果返回,我们前面有说到 layout 需要返回一个 MeasureResult 对象, 这个 layout 函数内部其实是一个 MeasureResult 对象,然后再通过 placeable.placeRelative(0, 0) 设置偏移,这里我们设置的是 0,那么就是原封不动的,组件大小和偏移都没有经过任何的修改,这个代码运行后是这样的
设置边距
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@Composable
fun layoutModifier2() {
BoxWithConstraints(modifier = Modifier
.background(Color.Green)
.layout { measurable, constraints -> // 测量本控件
// 增加 10 dp
val padding = 10.dp
val placeable = measurable.measure(
// 设置限制(约束)范围
constraints.copy(
/** 对上界和下界扩大 , 乘以 2,是因为,左右上下都有padding**/
/**
* 增加 padding, 这里的限制为什么要设减去 padding 呢 ?
*
* 这是因为当前控件增加了 padding, 那么它原本的可以用来放置内容的大小,被内边距给占用了,所以要减去
* 用一个例子来说,假设你有一个 Box,它的最大宽度是300dp,然后你决定添加50dp的 padding。那么,实际上,Box 内你能放置内容的最大宽度就变成了 300dp - 50dp*2 = 200dp。
* 相当于把 100dp 的空间给了 padding,真正用来放置内容的空间变小了。
*/
maxWidth = constraints.maxWidth - (padding * 2).roundToPx(),
maxHeight = constraints.maxHeight - (padding * 2).roundToPx(),
)
)
// 设置大小
layout(placeable.width + (padding * 2).roundToPx(), placeable.height + (padding * 2).roundToPx()) {
placeable.placeRelative(padding.roundToPx(), padding.roundToPx()) // 设置偏移
}
}) {
Text(text = "Hello World")
}
}
|
这里我们给这个控件增加左右上下 10dp 的边距间隙,首先是需要通过 measurable.measure() 进行测量,这里我们由于给当前控件增加 10dp 的间隙,当前的控件可以真正可显示的内容就少了 10dp,所以在设置 constraints 约束条件的时候,需要减去 10 dp,为什么要乘以 2,是因为左右上下都有 padding,然后通过 layout 将结果返回,然后再通过 placeRelative 进行 padding 像素的偏移,最终运行代码的效果是这样的
上面我们是自己手写了一个 padding 的实现过程,其实,我们可以看看官方的 Compose 的源代码中,Modifier.padding 基本也是这样做的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
private class PaddingNode(
var start: Dp = 0.dp,
var top: Dp = 0.dp,
var end: Dp = 0.dp,
var bottom: Dp = 0.dp,
var rtlAware: Boolean
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx() // 垂直和水平方向上的间距
val vertical = top.roundToPx() + bottom.roundToPx()
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) // 测量(减去padding)
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) { // 设置大小
if (rtlAware) { // 设置偏移
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
|
好了,基本上 layout 函数的使用大体就是如此。那么问题来了,设置 padding 像这样的,Compose 已经提供了,那 Modifier.layout 有什么使用场景呢?
能想到的一个场景有:是在原有的 Compose 组件内部已经都设置好了尺寸和位置了,而这个时候,我想额外设置这个组件的尺寸和偏移,比如如下例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Preview
@Composable
fun test() {
Box(modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(30, 0)
}
}) {
Box() {
Column {
Image(painter = painterResource(id = R.mipmap.aaa), contentDescription = "")
Text(text = "hello world !!! ")
}
}
}
}
|
这里内部组件的图片和文字的位置在实际的项目中,假设说是已经完美了,而这个时候,我想对这一整个整体进行偏移,那么可以使用 layout 函数进行设置,目前能想到的场景好像只有这个了,不过其实,单独设置 padding 也是可以做到的
关于 layout 这个函数,我觉得用到的场景是不多的,关键是要理解 layout 函数在测量过程中会对布局产生什么影响,有了之前的一些前奏知识,接下来我们再仔细看下它的测量流程是怎么样的
LayoutModifier 对布局产生的影响
在讲解 LayoutModifier 原理前先看下如下代码他们的组件大小是多少呢 ?
1
2
3
4
5
6
7
8
9
|
Box(Modifier.size(100.dp).size(200.dp))
Box(Modifier.size(200.dp).size(100.dp))
Box(
modifier = Modifier
.size(100.dp)
.background(Blue)
.size(50.dp)
.background(Origin)
)
|
LayoutNode 的测量过程
在讲解 LayoutModifier 之前,我们先要了解一个前置知识,那就是 Compose 在组合阶段,会将我们写的 Composable 函数最终组合成一个 LayoutNode 对象,而这个 LayoutNode 对象就是用来测量布局的,其中在 LayoutNode 类中测量和布局分别由 remeasure() 和 replace() 处理。
remeasure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
layout.kt
internal val layoutDelegate = LayoutNodeLayoutDelegate(this)
internal val measurePassDelegate
get() = layoutDelegate.measurePassDelegate
internal fun remeasure(
constraints: Constraints? = layoutDelegate.lastConstraints
): Boolean {
return if (constraints != null) {
if (intrinsicsUsageByParent == UsageByParent.NotUsed) {
// This LayoutNode may have asked children for intrinsics. If so, we should
// clear the intrinsics usage for everything that was requested previously.
clearSubtreeIntrinsicsUsage()
}
measurePassDelegate.remeasure(constraints) // 重要
} else {
false
}
}
|
其中,可以看到通过 measurePassDelegate 对象调用 remeasure 方法,然后 measurePassDelegate 对象又是 LayoutNodeLayoutDelegate 中的 MeasurePassDelegate 的一个内部类,所以最终的调用者是在 MeasurePassDelegate 中
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fun remeasure(constraints: Constraints): Boolean {
...
if (layoutNode.measurePending || measurementConstraints != constraints) {
...
performMeasure(constraints)
...
return sizeChanged
} else {
...
}
return false
}
|
然后再次调用 performMeasure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
private fun performMeasure(constraints: Constraints) {
layoutState = LayoutState.Measuring
measurePending = false
layoutNode.requireOwner().snapshotObserver.observeMeasureSnapshotReads(
layoutNode,
affectsLookahead = false
) {
outerCoordinator.measure(constraints) // 重要
}
if (layoutState == LayoutState.Measuring) {
markLayoutPending()
layoutState = LayoutState.Idle
}
}
|
在 performMeasure 中,又通过 outerCoordinator 调用了 measure 方法
1
2
3
|
interface Measurable : IntrinsicMeasurable {
fun measure(constraints: Constraints): Placeable
}
|
最终可以看到, 在这里的调用链就结束了, 再深入是一个接口了, 所以这个时候我们应该找到 outerCoordinator 的实现类是什么了
1
2
3
4
5
6
|
internal class LayoutNodeLayoutDelegate(
private val layoutNode: LayoutNode,
) {
val outerCoordinator: NodeCoordinator
get() = layoutNode.nodes.outerCoordinator
}
|
在这里可以看到, 这个 outerCoordinator 的获取方式是 layoutNode.nodes.outerCoordinator , 这个 layoutNode 我们上面说过了, 然后再看下 nodes 是什么
1
2
3
|
internal class LayoutNode(){
internal val nodes = NodeChain(this)
}
|
然后再看下 NodeChain ,因为 outerCoordinator 就在这个类中
1
2
3
4
5
6
7
8
9
10
11
12
13
|
internal class NodeChain(val layoutNode: LayoutNode) {
// 负责最内层测量的 NodeCoordinator
internal val innerCoordinator = InnerNodeCoordinator(layoutNode) // Composable
internal var outerCoordinator: NodeCoordinator = innerCoordinator
private set
internal val tail: Modifier.Node = innerCoordinator.tail
internal var head: Modifier.Node = tail
private set
private val isUpdating: Boolean get() = head === SentinelHead
private val aggregateChildKindSet: Int get() = head.aggregateChildKindSet
private var current: MutableVector<Modifier.Element>? = null
private var buffer: MutableVector<Modifier.Element>? = null
}
|
最终兜兜转转我们终于找到了 outerCoordinator 是个什么, 它是一个 NodeCoordinator 类型的对象, 他是用来测量和摆放布局的一个工具类,
这个时候,如果我们的组件一个 Modifier 都没有写的话, 获取的是 innerCoordinator , innerCoordinator 可以认为它是控件本身的一个测量布局的方式, 比如仅仅写一个 Text(),那他就是按照自身的测量规则
同时我们也可以发现 NodeChain 是一个双向链表, 因为它内部维护了 head tail 头尾指针, 到了这里, 我们可以大胆的猜测一些, 如果我们写了多个 LayoutModifier
那么是不是意味着都会被链在这个双向链表中呢 ?
确实是这样的, 在 Compose 组合阶段, 每一个 Composable 函数,都会被组合成一个 LayoutNode 对象, 然后它设置的 Modifier 会被包装成一个一个的 Modifier.Node 对象, 然后通过 NodeChain 串成一个双向链表
那么问题又来了, 我们设置的 Modifier 他是怎么被包装成 Modifier.Node 对象的呢 ? 又是怎么串成一个双向链表的呢 ?
其实答案在 LayoutNode 中的 Modifier 属性中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
override var modifier: Modifier = Modifier
set(value) {
field = value
nodes.updateFrom(value) // 重点
...
}
internal fun updateFrom(m: Modifier) {
var before = current
val beforeSize = before?.size ?: 0
// fillVector 会将 Modifier 展开铺平到一个数组,后面的代码就可以用这个数组遍历
val after = m.fillVector(buffer ?: mutableVectorOf())
if (after.size == beforeSize) {
var node: Modifier.Node? = paddedHead.child
while (node != null && i < beforeSize) {
val prev = before[i]
val next = after[i]
when (actionForModifiers(prev, next)) {
...
}
// NOTE: We do not need to check if the node is attached since these are all updated
// or reused modifiers only
node = node.child
i++
}
if (i < beforeSize) {
coordinatorSyncNeeded = true
structuralUpdate
i,
before,
after,
node,
layoutNode.isAttached,
)
}
} else if (!layoutNode.isAttached && beforeSize == 0) {
// 第一次组装双向链表
// 遍历上面铺平的数组,然后将 Modifier 装进 Node 组装成双向链表
coordinatorSyncNeeded = true
var node = paddedHead
while (i < after.size) {
val next = after[i]
val parent = node
// 组装双向链表的具体逻辑
node = createAndInsertNodeAsChild(next, parent)
logger?.nodeInserted(0, i, next, parent, node)
i++
}
syncAggregateChildKindSet()
} else if (after.size == 0) {
checkNotNull(before) { "expected prior modifier list to be non-empty" }
// common case where we we are removing all the modifiers.
var node = paddedHead.child
while (node != null && i < before.size) {
logger?.nodeRemoved(i, before[i], node)
node = detachAndRemoveNode(node).child
i++
}
innerCoordinator.wrappedBy = layoutNode.parent?.innerCoordinator
outerCoordinator = innerCoordinator
} else {
...
}
current = after
buffer = before?.also { it.clear() }
// 将头节点和尾节点保存到 head 和 tail
head = trimChain(paddedHead)
if (coordinatorSyncNeeded) {
// 将 Node 和所属的 NodeCoordinator 挂接关联
syncCoordinators()
}
}
|
这一步的逻辑是当我们在外部设置 Modifier 的时候, 那么就会调用到这里的 Modifier 的 set 函数, 然后调用 nodes.updateFrom(value) , 这个 value 就是我们组件设置的 Modifier, 一般来说多个的话,肯定是一个 CombinedModifier(这个我们之前的文章有提过),然后在 updateFrom 的内部,会通过 fillVector 方法,将我们外部设置的 Modifier 链铺平, 假设我们在外部设置的是 Modifier.size().padding().layout() 那么这里铺平后的集合就是这样的 sizeNode , paddingNode LayoutModifierNode
然后将铺平后的数据, 会通过 node = createAndInsertNodeAsChild(next, parent) 方法,将 nodeChain 这个双向链表给连起来, 最后也是比较重要的一步, 就是通过 syncCoordinators 方法,将设置好的 node
关联一个 NodeCoordinator 对象, 这个 NodeCoordinator 对象我们之前提到过, 就是用来测量布局的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
fun syncCoordinators() {
var coordinator: NodeCoordinator = innerCoordinator
var node: Modifier.Node? = tail.parent
while (node != null) {
val layoutmod = node.asLayoutModifierNode()
if (layoutmod != null) {
val next = if (node.coordinator != null) {
val c = node.coordinator as LayoutModifierNodeCoordinator
val prevNode = c.layoutModifierNode
c.layoutModifierNode = layoutmod
if (prevNode !== node) c.onLayoutModifierNodeChanged()
c
} else {
val c = LayoutModifierNodeCoordinator(layoutNode, layoutmod) // 创建 LayoutModifierNodeCoordinator
node.updateCoordinator(c)
c
}
coordinator.wrappedBy = next
next.wrapped = coordinator
coordinator = next
} else {
node.updateCoordinator(coordinator)
}
node = node.parent
}
coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
outerCoordinator = coordinator
}
|
这里的步骤就是给 node 对象设置一个 NodeCoordinator 对象,让每一个 LayoutModifier 都具备测量和布局的能力
到了这里我们之前分析的 outerCoordinator.measure(constraints),已经可以知道这个 outerCoordinator 就是一个 LayoutModifierNodeCoordinator 对象了,那么我们再来看下它的 measure 测量方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
override fun measure(constraints: Constraints): Placeable {
performingMeasure(constraints) {
with(layoutModifierNode) {
measureResult = if (this is IntermediateLayoutModifierNode) {
intermediateMeasure(
wrappedNonNull,
constraints,
lookaheadDelegate!!.measureResult.let { IntSize(it.width, it.height) },
lookaheadConstraints!!
)
} else {
measure(wrappedNonNull, constraints) // 最终调用的地方
}
this@LayoutModifierNodeCoordinator
}
}
onMeasured()
return this
}
|
1
2
3
4
5
6
7
|
// LayoutModifierNode.kt
interface LayoutModifierNode : DelegatableNode {
fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult
}
|
而这里调用的 measure 方法就是所属于 LayoutModifierNode 类中的方法了, 而 LayoutModifierNode 就是我们这是 padding,设置 size 的时候,它的内部方法实现在这个地方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
SizeNode
@Stable
fun Modifier.size(size: Dp) = this.then(
SizeElement( )
)
private class SizeElement(
private val minWidth: Dp = Dp.Unspecified,
private val minHeight: Dp = Dp.Unspecified,
private val maxWidth: Dp = Dp.Unspecified,
private val maxHeight: Dp = Dp.Unspecified,
private val enforceIncoming: Boolean,
private val inspectorInfo: InspectorInfo.() -> Unit
) : ModifierNodeElement<SizeNode>() {
override fun create(): SizeNode =
SizeNode(
minWidth = minWidth,
minHeight = minHeight,
maxWidth = maxWidth,
maxHeight = maxHeight,
enforceIncoming = enforceIncoming
)
override fun update(node: SizeNode) {
node.minWidth = minWidth
node.minHeight = minHeight
node.maxWidth = maxWidth
node.maxHeight = maxHeight
node.enforceIncoming = enforceIncoming
}
}
private class SizeNode(
var minWidth: Dp = Dp.Unspecified,
var minHeight: Dp = Dp.Unspecified,
var maxWidth: Dp = Dp.Unspecified,
var maxHeight: Dp = Dp.Unspecified,
var enforceIncoming: Boolean
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure( // sizeNode 的测量布局方式
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val wrappedConstraints = ....
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
// paddingNode
@Stable
fun Modifier.padding(all: Dp) = this then PaddingElement()
private class PaddingElement(
var start: Dp = 0.dp,
var top: Dp = 0.dp,
var end: Dp = 0.dp,
var bottom: Dp = 0.dp,
var rtlAware: Boolean,
val inspectorInfo: InspectorInfo.() -> Unit
) : ModifierNodeElement<PaddingNode>()
private class PaddingNode(
var start: Dp = 0.dp,
var top: Dp = 0.dp,
var end: Dp = 0.dp,
var bottom: Dp = 0.dp,
var rtlAware: Boolean
) : LayoutModifierNode, Modifier.Node() {
override fun MeasureScope.measure( // // PaddingNode 的测量布局方式
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
|
我们可以写一个更加简单的例子,可以更加清晰的看到这个过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
fun layoutModifier2() {
BoxWithConstraints(modifier = Modifier
.layout { measurable, constraints ->
println("@@@@ <top>.layoutModifier2 流程1")
val placeable = measurable.measure(constraints)
println("@@@@ <top>.layoutModifier2 流程2")
// 偏移
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
.layout { measurable, constraints ->
println("@@@@ <top>.layoutModifier2 流程3")
val placeable = measurable.measure(constraints)
println("@@@@ <top>.layoutModifier2 流程4")
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}) {
Text(text = "Hello World")
}
}
|
它的打印结果是这样的
1
2
3
4
|
@@@@ <top>.layoutModifier2 流程1
@@@@ <top>.layoutModifier2 流程3
@@@@ <top>.layoutModifier2 流程4
@@@@ <top>.layoutModifier2 流程2
|
所以这个流程应该这样就很清楚了,有点像一个链式调用,其实也有点像 okHttp 中的 process 的处理方式,在调用第一个 layout 进行测量的时候, 它会将约束条件传递到下一个 layout 函数中,而下一个 layout 函数,因为没有后续的 LayoutModifier 了,所以就不会再往内层传递了,而是测量完成后,将测量结果大小再一层一层的返回(如果左边的约束条件更加严格的话,则右边的尺寸将受到左边的约束)
到了这里,我们再来看看上面说的, 它们的结果是什么了
1
2
3
4
5
6
7
8
9
|
Box(Modifier.size(100.dp).size(200.dp)) // 最终大小为 100dp
Box(Modifier.size(200.dp).size(100.dp)) // 最终大小为 200dp
Box(
modifier = Modifier
.size(100.dp)
.background(Blue)
.size(100.dp)
.background(Red) // 大小为 100dp 的红色,盖着 100dp 的绿色
)
|
总结
通过上面的两个例子和原理的分析,在日常编写程序时我们就可以简单理解 LayoutModifier 对测量布局的影响是:当 Composable 有多个 LayoutModifier 时,最终会以最外层的 LayoutModifier 为最终结果(即是最左边的 LayoutModifier),最外层的优先级是最高的