在上一篇文章深入Jetpack Compose——布局原理与自定义布局(一) - 掘金 (juejin.cn) 中,我们大致了解了Layout过程并简单实现了两个自定义布局。本次让我们将目光转向Modifier和固有特性测量

本文部分参考自Android官方视频:Deep dive into Jetpack Compose layouts

Modifier

本质

关于Modifier的本质,RugerMc大佬在图解 Modifier 实现原理 ,竟然如此简单这篇文章中已经解释地非常清楚了,我就不画蛇添足了。不过为了后续行文方便,我还是在此简单说几点:

  1. Modifier 是个接口,包含三个直接实现类或接口:伴生对象 Modifier、内部子接口Modifier.ElementCombinedModifier
  2. 伴生对象Modifier是日常使用最多的,后面两者均为内部实现,实际开发中无需关注
  3. Modifier.xxx()方法实际上会创建一个Modifier接口的实现类的实例。如Modifier.size()会创建SizeModifer实例
1
2
3
4
5
6
@Stable
fun Modifier.size(size: Dp) = this.then(
SizeModifier(
/*省略具体细节*/
)
)
  1. Modifier.xxx().yyy().zzz()实际上会创建一个Modifier链,内部顺序遵循 xxx -> yyy -> zzz ,由CombinedModifier连接。Modifie接口提供了foldIn/foldOut方法允许我们顺序/逆序遍历到每个Modifier

    这里借上面文章中的一张图来说明:

    来源见水印,侵删

我们可以简单遍历一下看看,对于下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Composable
fun TraverseModifier() {
val modifier = Modifier
.size(40.dp)
.background(Color.Gray)
.clip(CircleShape)
LaunchedEffect(modifier){
// 顺序遍历Modifier
modifier.foldIn(0){ index , element : Modifier.Element ->
Log.d(TAG, "$index -> $element")
index + 1
}
}
}

它的输出为:

1
2
3
0 -> androidx.compose.foundation.layout.SizeModifier@78000000
1 -> Background(color=Color(...), brush=null, alpha = 1.0, shape=RectangleShape)
2 -> SimpleGraphicsLayerModifier(...)

作用

接下来,我们看看Modifier是怎么在布局中起作用的

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Composable
fun ModifierSample1() {
// 父元素
Box(modifier = Modifier
.width(200.dp)
.height(300.dp)
.background(Color.Yellow)){
// 子元素
Box(modifier = Modifier
.fillMaxSize()
.wrapContentSize(align = Alignment.Center)
.size(50.dp)
.background(Color.Blue))
}
}

它实际显示的效果如下

左上角的圆角是屏幕边缘

(左上角的圆角是屏幕边缘)

我们来逐步看看这到底是怎么发生的。这里我们选择子元素,也就是那个小一点的蓝色Box,来看看它的measureplace过程。

首先是measure。父元素明确了自身大小为200*300,该大小也就是子元素能占据的最大空间。因此

  1. 初始:初始约束 w:0-200, h:0-300

  2. fillMaxSize():占据最大空间,约束的min值更改为与max相同,即 w:200-200, h:300-300

  3. wrapContentSize():适应内容大小,约束的min值重新变回了0,即 w:0-200, h:0-300

  4. size():指定了精准大小。约束变为 w:50-50, h:50-50

  5. background():对大小约束无影响

最后,在Modifier的一顿操作之下,Box会收到一个 w:50-50, h:50-50 的约束。到这里走过的状态如下:

image-20220212180649407

接下来,Box内部的Layout微件执行measure方法得到了自己的大小:50*50。这个大小反向传回到Modifier链的最后一项,并开始place。接下来:

  1. background():此处略过
  2. size(50.dp):测得自己的大小:50*50,并据此创建自己的位置指令
  3. wrapContentSize():测得自己的大小:200*300,并知道自己的子元素大小50*50,且居中放置。据此创建自己的位置指令。
  4. fillMaxSize():解析自己的大小和位置

这个过程很类似于Layout微件,区别就是每个Modifier只有一个子元素(也就是Modifier链上的下一个元素)。事实上,如果看代码,你也很容易感受到二者的相似之处

wrapContentSize修饰符的代码举例,其实现类WrapContentModifiermeasure方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// 设置约束
val wrappedConstraints = Constraints(/**/)
// 测量得到可放置项
val placeable = measurable.measure(wrappedConstraints)
val wrapperWidth = placeable.width.coerceIn(constraints.minWidth, constraints.maxWidth)
val wrapperHeight = placeable.height.coerceIn(constraints.minHeight, constraints.maxHeight)
// layout函数放置到指定位置并返回结果
return layout(
wrapperWidth,
wrapperHeight
) {
val position = alignmentCallback(
IntSize(wrapperWidth - placeable.width, wrapperHeight - placeable.height),
layoutDirection
)
placeable.place(position)
}
}

怎么样,是不是很相似?

后续

关于Modifier我们就先看这些。下一篇,我们将接触固有特性测量这一特性,并改进我们在第一篇文章中实现的纵向布局。

本文所有代码见:此处