作者:FunnySaltyFish
https://juejin.cn/post/7063451846861406245
总览
Jetpack Compose 中,单个可组合项被显示出来,总体上经历三个过程
Composition(组合) -> Layout(布局) -> Drawing(绘制) 。
其中Layout阶段又存在两个方面的内容:Measure(测量) 和 Place(摆放)
今天我们主要着眼于 Layout 阶段,看看各个 Composable 是如何正确确定各自位置和大小的
Layout阶段主要做三件事情:
-
测量所有子微件的大小 -
确定自己的大小 -
正确摆放所有子元素的位置
为简化说明,我们先给出一个简单例子。该例子中,所有元素只需要遍历一次。
如下图的 SearchResult
微件,它的构成如下:

现在我们来看看Layout过程在这个例子中是什么情况
Measure
请求测量根布局,即 Row

Row
为了知道自己的大小,就得先知道自己的子微件有多大,于是请求 Image
和 Column
测量它们自己。对于Image,由于它内部没有其他微件,所以它可以完成自身测量过程并返回相关位置指令

接下来是 Column
,因为它内部有两个 Text
,于是请求子微件测量。而对于 Text
,它们也会正确返回自己的大小和位置指令

这时 Column
大小和位置指令即可正确确定。最后,Row
内部所有测量完成,它可以正确获得自己的大小和位置指令

测量阶段到此结束,接下来就是正确的摆放位置了
Place
完成测量后,微件就可以根据自身大小从上至下执行各子微件的位置指令,从而确定每个微件的正确位置。
现在我们把目光转向 Composition 阶段。大家平时写微件,内部都是由很多更基本的微件组合而来的,而事实上,这些基本的微件还有更底层的组成部分。如果我们展开刚刚的那个例子,它就成了这个样子

在这里,所有的叶节点(即没有子元素的节点)都是 Layout
这个微件。我们来看看这个微件吧
Layout Composable
此微件的签名如下:
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)
我们先看看第三个参数,这是之前从未见过的东西;而它恰恰控制着如何确定微件大小以及它们的摆放策略。
那来写个例子吧。我们现在自定义一个简单的纵向布局,也就是低配版 Column
自定义布局 – 纵向布局
写个框架
fun VerticalLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constrains: Constraints ->
}
}
Measurable
代表可测量的,其定义如下:
interface Measurable : IntrinsicMeasurable {
/**
* Measures the layout with [constraints], returning a [Placeable] layout that has its new
* size. A [Measurable] can only be measured once inside a layout pass.
*/
fun measure(constraints: Constraints): Placeable
}
可以看到,这是个接口,唯一的方法 measure
返回 Placeable
,接下来根据这个 Placeable
摆放位置。而参数 measurables
其实也就是传入的子微件形成的列表
而 Constraints
则描述了微件的大小策略,它的部分定义摘录如下:

举个栗子,如果我们想让这个微件想多大就多大(类似 match_parent
),那我们可以这样写:

如果它是固定大小(比如长宽50),那就是这样写

接下来我们就先获取 placeable
吧
val placeables = measurables.map { it.measure(constrains) }
在这个简单的例子中,我们不对 measure
的过程进行过多干预,直接测完获得有大小的可放置项。
接下来确定我们的 VerticalLayout
的宽、高。对于咱们的布局,它的宽应该容纳的下最宽的孩子,高应该是所有孩子之和。于是得到以下代码:
// 宽度:最宽的一项
val width = placeables.maxOf { it.width }
// 高度:所有子微件高度之和
val height = placeables.sumOf { it.height }
最后,我们调用 layout
方法返回最终的测量结果。前两个参数为自身的宽高,第三个 lambda 确定每个 Placeable
的位置
layout(width, height){
var y = 0
placeables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
这里用到了 Placeable.placeRelative
方法,它能够正确处理从右到左布局的镜像转换
一个简单的 Column 就写好了。试一下?
fun randomColor() = Color(Random.nextInt(255),Random.nextInt(255),Random.nextInt(255))
@Composable
fun CustomLayoutTest() {
VerticalLayout() {
(1..5).forEach {
Box(modifier = Modifier.size(40.dp).background(randomColor()))
}
}
}

嗯,工作基本正常。
接下来我们实现一个更复杂一点的:简易瀑布流
自定义布局—简易瀑布流
先把基本的框架撸出来,在这里只实现纵向的,横向同理
@Composable
fun WaterfallFlowLayout(
modifier: Modifier = Modifier,
content: @Composable ()->Unit,
columns: Int = 2 // 横向几列
) {
Layout(
modifier = modifier,
content = content,
) { measurables: List<Measurable>, constrains: Constraints ->
TODO()
}
}
我们加入了参数columns用来指定有几列。由于瀑布流宽度是确定的,所以我们需要手动指定宽度
val itemWidth = constrains.maxWidth / 2
val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth)
val placeables = measurables.map { it.measure(itemConstraints) }
在这里我们用新的 itemConstraints
对子微件的大小进行约束,固定了子微件的宽度。接下来就是摆放了。瀑布流的摆放方式其实就是看看当前哪一列最矮,就把当前微件摆到哪一列,不断重复就行
代码如下:
@Composable
fun WaterfallFlowLayout(
modifier: Modifier = Modifier,
columns: Int = 2, // 横向几列
content: @Composable ()->Unit
) {
Layout(
modifier = modifier,
content = content,
) { measurables: List<Measurable>, constrains: Constraints ->
val itemWidth = constrains.maxWidth / columns
val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth)
val placeables = measurables.map { it.measure(itemConstraints) }
// 记录当前各列高度
val heights = IntArray(columns)
layout(width = constrains.maxWidth, height = constrains.maxHeight){
placeables.forEach { placeable ->
val minIndex = heights.minIndex()
placeable.placeRelative(itemWidth * minIndex, heights[minIndex])
heights[minIndex] += placeable.height
}
}
}
}
这里用到了一个自定义的拓展函数 minIndex,作用是寻找数组中最小项的索引值,代码很简单,如下:
fun IntArray.minIndex() : Int {
var i = 0
var min = Int.MAX_VALUE
this.forEachIndexed { index, e ->
if (e<min){
min = e
i = index
}
}
return i
}
效果如下(设置列数为3):

本文代码:
https://github.com/FunnySaltyFish/JetpackComposeStudy/tree/master/app/src/main/java/com/funny/compose/study/ui/post_layout
~ END ~
推荐阅读
原文始发于微信公众号(AndroidPub):Compose 的布局原理与自定义布局
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/57462.html