回顾原生的自定义布局
Compose 中的自定义布局相较于原生的需求程度降低了很多,为什么呢?
原生的布局实现方式有两种:
- 使用 XML 文件来写
- 编写代码,动态地创建 View,并添加到 ViewGroup 中,这涉及了自定义 View
那什么时候我们会选择自定义 View 来写布局呢?主要有两种情况:
-
当 XML 不够方便时
比如我们现在想要修改某个组件的内容,但是它被嵌套进了布局深层,虽然我们可以通过多次
findViewById来获取这个组件的实例,不过这不仅麻烦还容易出错。这时,我们就可以通过自定义 View,提供 API 接口(如setPersonalSignatureText(text:String)),这样只需一行代码,就能修改组件内容。 -
当 XML 做不到时
比如我们想要一个组件在纵向填满可用高度,在这个前提下,让宽度与高度保持一致,你使用 XML 是做不到这一点的。只有当组件的宽高是固定的,你才能使用 XML 完成。这时你可以通过自定义 View 重写
onMeasure方法,来完成上述需求。又或者你不满足现有组件的布局时(如
LinearLayout),你也可以通过自定义 View,来重写onMeasure和onLayout方法,定制布局算法。onMeasure负责测量所有子 View 的尺寸,并根据子 View 的尺寸计算出自身的尺寸;onLayout则负责根据测量结果,确定每个子 View 的最终位置并进行摆放。
Compose 的自定义布局
而到了 Compose,所有界面的构建都是通过代码完成的,但这能叫自定义布局吗?像这样:
@Preview
@Composable
fun FakeCustomLayout() {
Column(Modifier.background(Color.White)) {
Text("我真的是自定义布局吗?")
Button(onClick = {}) {
Text("点击我,并没有什么效果")
}
}
}
如果这都叫自定义布局,那么 Compose 里所有的布局都是“自定义”布局了,这也就没有讨论的意义了。
更准确地说,虽然这也是在使用代码构建布局,但使用的都是现成的布局组件,这些布局规则都是固定的。所以 Compose 中的自定义布局指的是对布局算法的定制,我们自己去定义子组件如何被测量和摆放的规则。
这种“定制”布局的方式有两种:
-
Modifier.layout函数它可以通过影响单个 Composable 组件的测量和布局,来改变单个组件的尺寸和位置。例如给某个组件添加额外的边距。
-
LayoutComposable函数它可以控制子组件的测量和摆放逻辑,来改变每个子组件的尺寸和位置。从而我们就可以定制出我们想要的布局组件,比如一个类似
Column的布局。
因为 layout 函数我们之前已经讲解过了,所以接下来看看 Layout 函数。
Layout 的结构应该是什么样的?
首先它是用于自定义布局的,所以我们先创建一个自定义的 Composable 函数,在内部调用 Layout 函数:
@Composable
fun CustomLayout() {
// 调用 Layout Composable,传入其 lambda,包含测量和布局逻辑
Layout() { measurables, constraints -> // measurables: 子组件列表, constraints: 父布局约束
// layout() 函数用于确定自身尺寸并摆放子组件
// 初始尺寸暂定为 0,后续会根据子项实际测量结果计算
layout(width = 0, height = 0) {
// 此处进行子组件的摆放
}
}
}
其中 measurables 代表所有子组件的可测量对象,constraints 为父容器传递过来的约束。调用 layout() 函数是为了摆放子组件,并确定自身位置。
然后呢,为自定义 Composable 函数添加 content 参数,再提供给 Layout 函数,使我们的自定义布局能够接收外部传入的子组件,这是关键。因为你要有内容,才能对内容进行布局。
@Composable
fun CustomLayout(
content: @Composable () -> Unit // content 参数,用于从外部接收子 Composable
) {
// 将 content 传递给 Layout Composable
Layout(content = content) { measurables, constraints -> // measurables 现在会包含 content 中的组件
// 同样需要测量和布局
layout(width = 0, height = 0) {
// 子组件摆放逻辑
}
}
}
最后 Layout 还有一个可选参数 modifier,我们补充一下。
@Composable
fun CustomLayout(
modifier: Modifier = Modifier, // modifier 参数允许外部对自定义布局进行修饰
content: @Composable () -> Unit // content 参数用于接收子组件
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// measurables: 子组件的可测量对象列表
// constraints: 父容器传递的布局约束
// 核心逻辑:测量子组件,计算自身尺寸,摆放子组件
layout(width = 0 /* 实际宽度根据子项计算得出 */, height = 0 /* 实际高度根据子项计算得出 */) {
// 在这里根据测量结果放置子组件
}
}
}
这就是最终的结构了。
实现类似 Column 的布局
现在就来看看它的写法。
首先把内部的子组件都测量一下,并将每次测量的测量结果都保存到集合中。
// 测量所有子组件
val placeables: List<Placeable> = measurables.map { measurable ->
// 对每个子 Composable (measurable) 调用 measure 方法,并传入约束
// measure 方法会返回一个 Placeable 对象,它代表了已测量的子组件,包含其尺寸信息
measurable.measure(constraints)
}
然后计算出自身应有的尺寸,宽度为子组件的最大宽度,高度为所有子组件的高度之和。
// 计算自身尺寸 (基于子组件的测量结果)
// 宽度:取所有已测量子组件 (placeables) 中的最大宽度。如果列表为空,则宽度为0。
val width = placeables.maxOfOrNull { it.width } ?: 0
// 高度:所有已测量子组件的高度之和。
val height = placeables.sumOf { it.height }
最后填写自身的最终尺寸,并摆放所有子组件。
// 摆放子组件,并确定自身尺寸
// 调用 layout(width, height) 来设置当前自定义布局的最终尺寸
layout(width, height) {
var yPosition = 0 // 下一个子组件在垂直方向上的摆放位置
// 遍历所有已测量的子组件
placeables.forEach { placeable ->
// 计算当前子组件的 x 轴位置,使其水平居中
val xPosition = (width - placeable.width) / 2
// 放置子组件
placeable.placeRelative(xPosition, yPosition)
// 更新 yPosition,为下一个子组件的摆放做准备
yPosition += placeable.height // 每个子组件所在的高度依次递增
}
}
完整代码:
@Composable
fun CustomColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
// 测量所有子组件
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
// 计算自身尺寸 (基于子组件的尺寸)
// 宽度:取所有子组件中的最大宽度
val width = placeables.maxOfOrNull { it.width } ?: 0
// 高度:所有子组件高度之和
val height = placeables.sumOf { it.height }
// 摆放子组件,并确定自身尺寸
layout(width, height) {
var yPosition = 0
// 来确定每个子组件在其父布局中的位置,从上到下进行摆放
placeables.forEach { placeable ->
// 所有子组件水平居中对齐
val xPosition = (width - placeable.width) / 2
placeable.placeRelative(xPosition, yPosition)
yPosition += placeable.height
}
}
}
}
然后测试一下:
@Preview
@Composable
private fun CustomColumnPrev() {
CustomColumn(Modifier.padding(12.dp)) {
Text(text = "四十年来家国,三千里地山河。", modifier = Modifier.background(Color.Cyan))
Text(text ="凤阁龙楼连霄汉,玉树琼枝作烟萝,几曾识干戈?", modifier = Modifier.background(Color.Green))
Text(text ="一旦归为臣虏,沈腰潘鬓消磨。", modifier = Modifier.background(Color.LightGray))
Text(text ="最是仓皇辞庙日,教坊犹奏别离歌,垂泪对宫娥。", modifier = Modifier.background(Color.DarkGray))
}
}
预览效果: