原创:写给初学者的Jetpack Compose教程,高级Layout
大家好,写给初学者的Jetpack Compose教程又更新了。
在本系列上一篇文章 写给初学者的Jetpack Compose教程,derivedStateOf 的留言中,有位读者朋友说,想要让我写一篇关于IntrinsicSize的文章,官方文档看得似懂非懂。
我的这个Compose系列本来就没有非常严谨的规划,经常是我自己学到了哪里就写哪里。如果大家有特别想看的内容,也可以告诉我,我尽量帮大家给安排上。
关于这个IntrinsicSize,我专门去学习了一下,它的内容其实并不复杂,甚至单独拿出来写一篇文章我认为有点太简单了。所以这篇文章我准备讲一讲Compose的高级Layout知识,顺便引出IntrinsicSize的相关介绍。
Compose的基础控件和布局在 写给初学者的Jetpack Compose教程,基础控件和布局 这篇文章中已经有比较详细的讲解了,但是这篇文章中使用的都是Compose内置好的布局,如Column、Row、Box等。那么如果我们想要编写一个自定义布局该怎么办呢?这就是本篇文章要介绍的内容了。
首先搬出一张来自官网的Compose工作流程示意图:
可以看到,客户端UI框架的工作,无非就是要把各种各样的数据,转换成UI界面呈现给用户。
至于如何转换呢?主要分成了3个步骤,Composition、Layout和Drawing。
Composition这一步其实就是我们平时写的各种Composable函数,用于告知Compose我们想要编写出一个怎样的界面,最终会构建成一棵UI树(见下图)。对应到View系统中,那就是编写XML这部分。
Layout这一步则是对构建好的UI树进行遍历,测量树中每个节点的尺寸以及在屏幕中放置的位置。对应到View系统中,那就是onMeasure()和onLayout()这部分。
最后,Drawing这一步就是把测量好的所有节点绘制到屏幕上。对应到View系统中,那就是onDraw()这部分。
之前本系列文章的主要内容都是聚焦在Composition这一块,今天就让我们调整一下关注点,来看一看Layout这部分的知识。
Compose的Layout阶段由两部分组成,分别是Measure和Place,也就是测量和放置。
在Layout阶段,Compose的执行逻辑遵循以下规则:
测量当前布局下每个子控件的尺寸。 根据子控件测量的结果约定自身的尺寸。 将子控件放置在合理的位置上。
测量Row布局下的所有子控件,也就是Image和Column Image被测量,并且由于Image没有子控件了,因此它可以直接决定自身的尺寸,并返回给Row。 Column被测量,由于Column下还有两个Text子控件,因此Column的尺寸暂时是不能决定的,要先测量子控件才行。 第一个Text被测量,由于Text没有子控件了,因此它可以直接决定自身的尺寸,并返回给Column。 第二个Text被测量,由于Text没有子控件了,因此它可以直接决定自身的尺寸,并返回给Column。 Column根据子控件返回的尺寸来决定自身的尺寸,并将两个Text子控件按纵向进行放置,最后返回给Row。 Row根据子控件返回的尺寸来决定自身的尺寸,并将Image和Column两个子控件按横向进行放置。
@Composable
fun MyColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map {
it.measure(constraints)
}
val layoutWidth = placeables.maxOf { it.width }
val layoutHeight = placeables.sumOf { it.height }
layout(layoutWidth, layoutHeight) {
var y = 0
for (placeable in placeables) {
placeable.placeRelative(x = 0, y = y)
y += placeable.height
}
}
}
}
测量当前布局下每个子控件的尺寸。 根据子控件测量的结果决定自身的尺寸。 将子控件放置在合理的位置上。
@Composable
fun SimpleWidgetColumn() {
MyColumn(modifier = Modifier.wrapContentSize(align = Alignment.TopStart)) {
for (i in 1..5) {
Button(onClick = {}) {
Text(
text = "This is Button $i",
color = Color.White,
)
}
}
}
}
@Composable
fun SimpleTextRow(modifier: Modifier = Modifier) {
Box(modifier.fillMaxWidth()) {
Text(
text = "Text 1",
color = Color.Blue,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopStart)
)
Divider(
modifier = modifier
.width(8.dp)
.height(40.dp)
.align(Alignment.TopCenter),
color = Color.Green
)
Text(
text = "Text 2",
color = Color.Red,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopEnd)
)
}
}
@Composable
fun SimpleTextRow(modifier: Modifier = Modifier) {
Box(modifier.fillMaxWidth()) {
Text(
text = "Text 1\nText 1\nText 1\nText 1\nText 1\nText 1",
color = Color.Blue,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopStart)
)
Divider(
modifier = modifier
.width(8.dp)
.height(40.dp)
.align(Alignment.TopCenter),
color = Color.Green
)
Text(
text = "Text 2\nText 2\nText 2\nText 2\nText 2\nText 2",
color = Color.Red,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopEnd)
)
}
}
@Composable
fun SimpleTextRow(modifier: Modifier = Modifier) {
Box(modifier.fillMaxWidth()) {
...
Divider(
modifier = modifier
.width(8.dp)
.fillMaxHeight()
.align(Alignment.TopCenter),
color = Color.Green
)
...
}
}
@Composable
fun SimpleTextRow(modifier: Modifier = Modifier) {
Box(modifier.fillMaxWidth().height(IntrinsicSize.Max)) {
Text(
text = "Text 1\nText 1\nText 1\nText 1\nText 1\nText 1",
color = Color.Blue,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopStart)
)
Divider(
modifier = modifier
.width(8.dp)
.fillMaxHeight()
.align(Alignment.TopCenter),
color = Color.Green
)
Text(
text = "Text 2\nText 2\nText 2\nText 2\nText 2\nText 2",
color = Color.Red,
fontSize = 30.sp, modifier = modifier.align(Alignment.TopEnd)
)
}
}
@Composable
fun MyColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
val measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
...
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
...
}
}
Layout(modifier = modifier, content = content, measurePolicy = measurePolicy)
}