Compose 的布局原理与自定义布局

作者:FunnySaltyFish
https://juejin.cn/post/7063451846861406245

总览

Jetpack Compose 中,单个可组合项被显示出来,总体上经历三个过程

Composition(组合) -> Layout(布局) -> Drawing(绘制)  。

其中Layout阶段又存在两个方面的内容:Measure(测量)Place(摆放)

今天我们主要着眼于 Layout 阶段,看看各个 Composable 是如何正确确定各自位置和大小的

Layout阶段主要做三件事情:

  • 测量所有子微件的大小
  • 确定自己的大小
  • 正确摆放所有子元素的位置

为简化说明,我们先给出一个简单例子。该例子中,所有元素只需要遍历一次。

如下图的 SearchResult 微件,它的构成如下:

Compose 的布局原理与自定义布局

现在我们来看看Layout过程在这个例子中是什么情况

Measure

请求测量根布局,即 Row

Compose 的布局原理与自定义布局

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

Compose 的布局原理与自定义布局

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

Compose 的布局原理与自定义布局

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

Compose 的布局原理与自定义布局

测量阶段到此结束,接下来就是正确的摆放位置了

Place

完成测量后,微件就可以根据自身大小从上至下执行各子微件的位置指令,从而确定每个微件的正确位置。

现在我们把目光转向 Composition 阶段。大家平时写微件,内部都是由很多更基本的微件组合而来的,而事实上,这些基本的微件还有更底层的组成部分。如果我们展开刚刚的那个例子,它就成了这个样子

Compose 的布局原理与自定义布局

在这里,所有的叶节点(即没有子元素的节点)都是 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 则描述了微件的大小策略,它的部分定义摘录如下:

Compose 的布局原理与自定义布局

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

Compose 的布局原理与自定义布局

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

Compose 的布局原理与自定义布局

接下来我们就先获取 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()))
        }
    }
}
Compose 的布局原理与自定义布局

嗯,工作基本正常。

接下来我们实现一个更复杂一点的:简易瀑布流

自定义布局—简易瀑布流

先把基本的框架撸出来,在这里只实现纵向的,横向同理

@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):

Compose 的布局原理与自定义布局

本文代码:

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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!