共计 13810 个字符,预计需要花费 35 分钟才能阅读完成。
在上一篇中,咱们不仅理解了 Compose 中的 Column、Row、Box 等几种常见的布局形式 还学习了 CompositionLocal 类在 Compose 中进行传值的办法;还有可疾速搭建 App 构造的 Scaffold 脚手架组件,顺便学习了 Surface、Modifier 的一些应用,还有 ConstraintLayout 在 Compose 中的应用办法。尽管官网提供了这么多 Compose 组件,但在理论需要开发中,定制化组件依然必不可少。
在传统的 View 体系中,零碎为开发者提供了许多能够间接应用的组件 View,比方:TextView、ImageView、RelativeLayout 等。咱们也能够通过自定义 View 来创立一些零碎没有提供给咱们的、具备非凡性能的 View。Compose 当然也不甘落后,在 Compose 中咱们能够应用 Layout 组件来自定义咱们本人的 Composable 组件。实际上,所有相似于 Column、Row 等组件底层都是用 Layout 进行扩大实现的。
在 View 体系中,自定义 View 最为常见的两种状况是:
1)继承已有 View 进行性能扩大,例如继承 TextView 或间接继承 View 进行改写;
2)继承 ViewGroup,并重写父类的 onMeasure 和 onLayout 办法。而在 Compose 中咱们只须要简略地应用 Layout 组件自定义就能够了。
在开始之前,咱们须要先理解一下 Layout Composable 组件的一些基础知识。
1. Compose 自定义 Layout 的根本准则
在 Compose 中,一个 Composable 办法被执行时,会被增加到 UI 树中,而后会被渲染展现在屏幕上。这个 Composable 办法咱们能够看成是一个 View 零碎中的布局,在 Compose 中称为 Layout。每个 Layout 都有一个 parent Layout 和 0 个或多个 children,这跟 View 体系很像。当然,这个 Layout 本身含有在它的 parent Layout 中的地位信息,包含地位坐标 (x, y)
和它的尺寸大小 width
和height
。
Layout 中的 children Layout 子元素会被调用去测量它们本身的大小,同时须要满足规定的 Constraints 束缚。这些 Constraints 束缚限度了 width
和 height
的最大值和最小值。当 Layout 把本人的 children Layout 测量实现之后,它本人的尺寸才会确定下来,又是递归。。。一旦一个 Layout 元素实现本身的测量,它就能够将本人的 children 依据 Constraints 束缚在本人的空间中进行摆放了。是不是跟 View 体系一样?先测量后摆放。
OK,最重要的来了!Compose UI 不容许屡次测量。 Layout 元素为了尝试不同的测量设置,它不能屡次测量其任何子元素。单次测量 (Single-pass measurement) 当然会晋升渲染效率,尤其是在 Compose 解决深度较大的 UI 树时。如果一个 Layout 元素须要测量两次它的所有子元素,子元素中的子元素就会被测量四次,以此类推,测量的次数就会随着布局深度成指数级增长!其实 View 体系就是这样的,所以在 View 体系中开发肯定要缩小布局的层数!不然在须要反复测量的状况下,渲染效率将会及其低下。所以 Compose 中才做了不容许屡次测量的限度,然而,在有些场景下,咱们又是须要获取到子元素屡次测量并获取信息的。对于这些状况,还是有办法做到屡次测量的,限于篇幅起因,前面有空再说~
Compose 中自定义一个控件(官网称之为 Layout)也有两种状况:
- 自定义 Layout 没有其余子元素,就只是它本人自身,相似于 View 体系中的“自定义 View”;
- 自定义 Layout 有子元素,须要思考子元素的摆放地位,相似于 View 体系中的“自定义 ViewGroup”。
咱们先来看第一种状况。
2. Compose 自定义一个“View”
Compose 中的自定义 Layout 跟 View 体系是很不同的。咱们须要自定义的 Layout 竟然就是自定义一个 Modifier 属性!就是去本人实现 Modifier 中 Layout 办法,去实现如何测量以及搁置它本人自身即可。一个常见的自定义 Layout Modifier 的构造代码如下:
// code 1
fun Modifier.customLayoutModifier(...) { // 能够自定义一些属性
Modifier.layout { measurable, constraints ->
... // 在这里须要本人实现 测量 和 搁置的办法
}
}
能够看进去,要害就是 Modifier.layout 办法,它有两个 lambda 表达式:
measurable
:用于子元素的测量和地位搁置的;constraints
:用于束缚子元素 width 和 height 的最大值和最小值。
举个简略的栗子进行阐明。一个一般的 Text 组件只能调整文案的边缘离 Text 组件上下左右四边缘的间隔,例如图 1 所示。这个 Text 只能设置周围的 padding 值,高低我设置的 15dp,左右设置的 30dp。
如果我想管制文案的底部 baseline 离 Text 上边距的间隔呢?啥是底部 baseline?这就须要理解一下 Android 在绘制文案时的算法了。
从图 2 能够看出,Android 绘制文案时,baseline 决定了文案主体的底部地位。Compose 中的 Text 只能通过 Modifier.padding 设置 leading 离 Text 组件顶部的间隔。而这里咱们自定义的 Layout 须要满足可设置 Baseline 离 Text 顶部的间隔。即下图图 3 中上方的成果,怎么做呢?
首先当然就是测量啦,记住 Layout 只能测量它的子元素一次。在 code1 中调用 measure 办法,就能够测量了:
// code 2
fun Modifier.firstBaselineToTop( // firstBaselineToTop 就是你自定义的 modifier 的办法名
firstBaselineToTop: Dp // 自定义 modifier 办法中的参数,这里就是一个
) = this.then(
layout { measurable, constraints -> // 调用 layout 办法去测量和搁置子元素组件
val placeable = measurable.measure(constraints) // 首先是测量
...
}
)
当调用 measurable 的 measure 办法后,就会返回一个 Placeable 对象。在这里,咱们能够将 layout 中的 constraints 约束条件传递给 measure 办法,或者传入咱们自定义的约束条件的 lambda。因为在这个场景下咱们不须要再去对测量进行任何的限度,所以间接传入 layout 中给的 constraints 即可。总之,这一步就是为了失去这个 Placeable 对象,拿到这个之后就能够在前面调用 Placeable 对象的 placeRelative 办法对子元素进行地位的摆放了!
OK,当初曾经对 Composable 组件进行了测量,而后咱们就能够调用 layout(width, height) 办法去依据测量的尺寸来搁置内容。width 不必求,间接用测量得来的 width 就行,要害就是如何求出传入 layout 办法的 height 值,看代码再来说吧:
// code 3
fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// 查看这个 Composable 组件是否存在 FirstBaseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
// 存在的状况下,获取 FirstBaseline 离 Composable 组件顶部的间隔
val firstBaseline = placeable[FirstBaseline]
// 计算 Y 轴方向上 Composable 组件的搁置地位
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
// 计算得出此 Composable 组件真正的 height 值
val height = placeable.height + placeableY
layout(placeable.width, height) {...}
}
)
说实话最后看到这段代码也是懵逼了良久。。。
首先 check 办法相似于一个 assert 断言,如果外面的后果是 false 则会抛出一个 IllegalStateException 异样。这里是查看下被咱们自定义的 Modifier 润饰的 Composable 组件是否存在 FirstBaseline 属性,Text 组件里是存在 baseline 的,如果不存在当然就不能用咱们自定义的这个 firstBaselineToTop Modifier 了。
存在的状况下,再去获取这个 Baseline 与 此组件顶部的间隔,也就是图 4 中 c 的长度。图中蓝色框代表的是一般的 Text 组件所占的空间地位;彩色框代表的是屏幕边缘;红色虚线代表的是 Text 中的 Baseline。a 示意的就是咱们自定义的 Modifier.firstBaselineToTop 办法的 firstBaselintToTop 参数。咱们的指标就是能够依据传入的 firstBaselintToTop 参数计算出 Text 组件在 Y 轴上的摆放地位,以及真正的 width 和 height 值大小。
之前在 layout 办法中调用了 measurable 的 measure 办法测量的是一般 Text 组件的宽高,即图 4 中蓝色框的宽高,而咱们自定义的 Layout 的宽高则是图中用橙色和绿色标注的宽高尺寸。width 间接由 Placeable 对象就可取得(placeable.width),而高度由示意图能够得出计算方法:height = placeable.height + d
,即一般 Text 的高度再加上 d,d = a – c,即 d = firstBaselintToTop - baseline
。所以,d 就是 placeableY 参数。终于看懂 code 3 了,原来就是为了算出自定义 Layout 的 width 和 height,而后通过 layout 办法进行设置啊!
接下来就是地位的搁置了。调用 Placeable 对象的 placeRelative 办法即可:
// code 4
fun Modifier.firstBaselineToTop(firstBaselineToTop: Dp) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {placeable.placeRelative(0, placeableY)
}
}
)
留神,自定义 Layout 必须调用 placeRelative 办法,否则该自定义 Layout 将不可见。 placeRelative 办法会依据以后的 layoutDirection 布局方向对自定义 Layout 主动进行地位调整。在这里咱们自定义的 Layout 摆放比较简单,就是 Y 轴上有个偏移量,X 轴上没有偏移,看图 2 也可直观得悉。
那么如何应用呢?想必你们也猜到了,就跟之前应用其余 Modifier 办法润饰 Text 或其余 Composable 组件一样应用就好:
// code 5
@Composable
fun CustomLayoutDemo() {
Row {
Text(
text = "我是栗子 1",
modifier = Modifier.firstBaselineToTop(40.dp),
fontSize = 20.sp
)
Spacer(modifier = Modifier.width(20.dp))
Text(
text = "我是栗子 2",
modifier = Modifier.firstBaselineToTop(40.dp),
fontSize = 15.sp
)
Spacer(modifier = Modifier.width(20.dp))
Text(
text = "我是栗子 3",
modifier = Modifier.firstBaselineToTop(40.dp),
fontSize = 30.sp
)
}
}
在 code 5 中别离展现了 3 个 Text,都应用了咱们自定义的 Modifier 修饰符 firstBaselineToTop,且设置的参数都是 40dp,不同的是字号。从图 5 的显示成果来看,达到了咱们想要的自定义 Layout 的成果,即尽管字号大小不同,然而每个 Text 中文案的 Baseline 离自定义 Layout 的顶部间隔是一样的。
3. 自定义一个“ViewGroup”
说完了 Compose 自定义“View”的办法,当然也就少不了自定义“ViewGroup”了。其实,Compose 中的 Row、Column 组件都是应用 Layout 办法实现的,它也是 Compose 用来自定义一个“ViewGroup”的外围办法。咱们能够通过 Layout 组件手动地对它其中的子元素进行测量和摆放,一个自定义“ViewGroup”的 Layout 代码构造通常如下代码所示:
// code 6
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
// 此处可增加自定义的参数
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
){ measurable, constraints ->
// 对 children 进行测量和搁置
···
}
}
对于一个自定义 Layout 来说,,起码须要三个参数:
- modifier:由内部传入的修饰符,用来润饰咱们自定义的这个 Layout 组件的一些属性或 Constraints;
- content:咱们自定义的这个 Layout 组件中所蕴含的子元素 children;
- measurePolicy:相熟 Kotlin 语法的同学们会晓得,code 6 中 Layout 后跟着的 lambda 表达式其实也是 Layout 的一个参数,从字面意思上也可晓得,这个是为了对 children 进行测量和摆放操作的。默认场景下只实现 measure 办法即可,当咱们想让咱们自定义的 Layout 组件适配 Intrinsics(官网称之为 固有个性测量)时,就须要重写 minIntrinsicWidth、minIntrinsicHeight、maxIntrinsicWidth、maxIntrinsicHeight 办法。篇幅起因当前再说哈~
这里咱们用 Layout 组件自定义一个根本的简略的 Column 组件,用于竖直方向上摆放子元素,咱们取名为 MyOwnColumn。如之前所述的,咱们第一件事就是测量 children,并且只能测量一次。与之前的自定义“View”不同的是,这里须要测量的不是它自身的尺寸,而是测量它其中蕴含的所有 children 的尺寸:
// code 7
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
// 此处可增加自定义的参数
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
// 对 children 进行测量和搁置
val placeables = measurables.map { measurable ->
// 测量每个 child 的尺寸
measurable.measure(constraints)
}
...
}
}
能够看出,在 map 里每个 child 都调用 measure 办法进行了测量,并且与之前一样,咱们无需再针对测量进行限度,所以间接传入 Layout 中的 constraints 即可。到这里,咱们曾经测量了所有的 children 子元素。
在设置这些 children 的地位之前,咱们还须要依据测量的 children 尺寸来计算得出咱们自定义的 MyOwnColumn 组件本身的宽高了。上面代码是尽最大可能地设置咱们自定义的 MyOwnColumn 的 Layout 尺寸:
// code 8
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
// 此处可增加自定义的参数
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
// 对 children 进行测量和搁置
val placeables = measurables.map { measurable ->
// 测量每个 child 的尺寸
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
// 摆放 children
...
}
}
}
最初就能够对 children 进行摆放了。与上述的自定义“View”雷同,咱们也是调用 placeable.placeRelative(x,y)
来搁置地位。因为是自定义一个 Column,须要竖直方向上一个个进行摆放,所以每个 child 程度方向上 x 必定从最右边开始,设置为 0。而竖直方向上须要一个变量记录下一个 child 在竖直方向上的地位值。具体代码如下:
// code 9
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
// 此处可增加自定义的参数
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
// 对 children 进行测量和搁置
val placeables = measurables.map { measurable ->
// 测量每个 child 的尺寸
measurable.measure(constraints)
}
var yPosition = 0 // 记录下一个元素竖直方向上的地位
layout(constraints.maxWidth, constraints.maxHeight) {
// 摆放 children
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, yPosition)
yPosition += placeable.height
}
}
}
}
留神一下咱们自定义的这个 Column 的宽高设置的是尽最大可能撑满父布局:layout(constraints.maxWidth, constraints.maxHeight)
,所以跟官网的 Column 是有很大的不同的。这里只是为了阐明 Compose 中自定义一个“ViewGroup”的办法流程。
MyOwnColumn 在应用上与 Column 统一,只是占用父布局空间的策略不一样。官网的 Column 布局默认状况下宽高是尽可能小的占用父布局,相似于 wrap_content;而 MyOwnColumn 是尽可能大的占用父布局,相似于 match_parent。下图图 6 也能够分明地看到成果。
// code 10
@Composable
fun MyOwnColumnDemo() {MyOwnColumn(Modifier.padding(20.dp)) {Text("我是栗子 1")
Text("我是栗子 2")
Text("我是栗子 3")
}
}
比照一下 Compose 中的自定义 Layout 的两种形式,一种是针对某个组件进行的性能扩大,相似于 View 体系中对某个已有的 View 或间接继承 View 进行的自定义,它其实是自定义一个 Modifier 办法;另一种是针对某个容器组件的自定义,相似于 View 体系中对某个已有的 ViewGroup 或间接继承 ViewGroup 进行自定义,它其实就是一个 Layout 组件,是布局的次要外围组件。接下来让咱们看看更加简单的自定义 Layout。
4. 自定义简单的 Layout
OK,理解了 Compose 自定义 Layout 的根本办法步骤,让咱们看看一个略微简单的栗子。如果须要实现一个横向滑动的瀑布流布局,例如下图两头局部所示:
能够设置展现成多少行,这里是展现成 3 行,咱们只须要传入所有的子元素即可。现有的官网 Compose 组件中没有这种性能的组件,这就须要定制化了。先依照之前的模板代码构建一下框架:
// code 11
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3, // 自定义的参数,管制展现的行数,默认为 3 行
content: @Composable () -> Unit){
Layout( // 次要还是这个 Layout 办法
modifier = modifier,
content = content
) { measurables, constraints ->
// 测量和地位摆放逻辑
}
}
接下来还是那个流程:
1)测量所有子元素尺寸;
2)计算自定义 Layout 的尺寸;
3)摆放子元素。这里只展现 Layout 办法中的代码:
// code 12
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 用于记录每一行的宽度信息
val rowWidths = IntArray(rows){0}
// 用于记录每一行的高度信息
val rowHeights = IntArray(rows){0}
val placeables = measurables.mapIndexed { index, measurable ->
// 规范流程:测量每个 child 尺寸,取得 placeable
val placeable = measurable.measure(constraints)
// 依据序号给每个 child 分组,记录每一组的宽高信息
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = max(rowHeights[row], placeable.height)
placeable // 测量完了要记得返回 placeable 对象
}
...
}
接下来,就是计算自定义 Layout 本身的尺寸了。通过下面的操作,咱们曾经得悉每行 children 的最大高度,那么所有行高度相加就能够失去自定义 Layout 的高度了;而所有行中宽度最大值就是自定义 Layout 的宽度了。此外,咱们还失去了每一行在 Y 轴上的地位了。相干的代码如下:
// code 13
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
...
// 自定义 Layout 的宽度取所有行中宽度最大值
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
?: constraints.minWidth
// 自定义 Layout 的高度当然为所有行高度之和
val height = rowHeights.sumOf {it}
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// 计算出每一行的元素在 Y 轴 上的摆放地位
val rowY = IntArray(rows) {0}
for (i in 1 until rows) {rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// 设置 自定义 Layout 的宽高
layout(width, height) {
// 摆放每个 child
...
}
}
咦?是不是又和你设想中的代码不太一样?在求宽度 width 时,它还应用了 coerceIn 办法对 width 进行了限度,限度 width 在 constraints 束缚的最小值和最大值之间,如果超出了则会被设置成最小值或最大值。height 也是如此。而后还是调用的 layout 办法来设置咱们自定义 Layout 的宽高。
最初,就是调用 placeable.placeRelative(x, y)
办法将咱们的 children 摆放到屏幕上即可。当然,还是须要借助变量存储 X 轴 上的地位信息的。具体代码如下:
// code 14
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
...
// 计算出每一行的元素在 Y 轴 上的摆放地位
val rowY = IntArray(rows) {0}
for (i in 1 until rows) {rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// 设置 自定义 Layout 的宽高
layout(width, height) {
// 摆放每个 child
val rowX = IntArray(rows) {0} // child 在 X 轴的地位
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(rowX[row],
rowY[row]
)
rowX[row] += placeable.width
}
}
}
代码逻辑比较简单,不再多做什么解释。综上,残缺的这个自定义 Layout 的代码如下:
// code 15
// 横向瀑布流自定义 layout
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3, // 自定义的参数,管制展现的行数,默认为 3 行
content: @Composable () -> Unit) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 用于记录每一横行的宽度信息
val rowWidths = IntArray(rows) {0}
// 用于记录每一横行的高度信息
val rowHeights = IntArray(rows) {0}
// 测量每个 child 尺寸,取得每个 child 的 Placeable 对象
val placeables = measurables.mapIndexed { index, measurable ->
// 规范流程:测量每个 child 尺寸,取得 placeable
val placeable = measurable.measure(constraints)
// 依据序号给每个 child 分组,记录每一个横行的宽高信息
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = max(rowHeights[row], placeable.height)
placeable // 这句别忘了,返回每个 child 的 placeable
}
// 自定义 Layout 的宽度取所有行中宽度最大值
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
?: constraints.minWidth
// 自定义 Layout 的高度当然为所有行高度之和
val height = rowHeights.sumOf {it}
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// 计算出每一行的元素在 Y 轴 上的摆放地位
val rowY = IntArray(rows) {0}
for (i in 1 until rows) {rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// 设置 自定义 Layout 的宽高
layout(width, height) {
// 摆放每个 child
val rowX = IntArray(rows) {0} // child 在 X 轴的地位
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(rowX[row],
rowY[row]
)
rowX[row] += placeable.width
}
}
}
}
OK,再写一个小的组件作为 children 子元素,用来显示,具体代码如下:
// code 16
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Magenta, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
还有一点须要留神的是,咱们自定义的 Layout StaggeredGrid 的宽度是会超出屏幕的,所以在理论应用中,还得增加一个 Modifier.horizonalScroll 用于程度方向上滑动,这样才用着难受~ 理论应用的代码样例如下:
// code 17
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
Row(modifier = Modifier.horizontalScroll(rememberScrollState())){StaggeredGrid() {for (topic in topics) {Chip(modifier = Modifier.padding(8.dp),text = topic)
}
}
}
当然,还反对本人设置须要展现成几行的款式,这里默认值为 3 行。
总结一下,在 Compose 中自定义 Layout 的根本流程其实跟 View 体系中自定义 View 的一样,其中最大的不同就是在测量的步骤,Compose 为提高效率不容许屡次进行测量。而且 Compose 的自定义 Layout 的两种状况也能够对应到 View 体系中的两个状况,但能够看出,Compose 都是在 Layout 组件中进行的改写与编程,能够让开发者更加聚焦在具体的代码逻辑上,这也是 Compose 自定义 Layout 的劣势所在。那么,Compose 的自定义“View”,你学会了么?
更多内容,欢送关注公众号:修之竹
或者查看 修之竹的 Android 专辑
赞人玫瑰,手留余香!欢送点赞、转发~ 转发请注明出处~
参考文献
- https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#6
- 大海螺 Utopia。《Android 文字基线(Baseline)算法》. https://www.jianshu.com/u/79e66729b5ec
- Jetpack Compose 博物馆 – 自定义 Layout. https://compose.net.cn/layout/custom_layout/
- https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#7