Compose 自定义 - 布局(测量+摆放) Layout

文章详细介绍了AndroidCompose中关于组件的测量和摆放过程,包括Compose如何避免多次测量以提升性能,以及如何通过Modifier.layout()自定义布局。同时,文章讨论了固有特性测量机制,如IntrinsicMeasurement,用于在正式测量前获取子元素的最大或最小尺寸,以解决复杂的布局问题。
摘要由CSDN通过智能技术生成

参考文章

一、概念

1.1 过程

测量和摆放是一个连续的过程,在 View 体系中分开是因为可能会对子元素进行多次测量:例如父容器宽度是 wrap_content 会依次测量所有子元素选取最宽的作为自己宽度,如果有子元素的宽度是 match_parent 会先将该子元素保存起来(先以0为强制宽度测量)并正常测量其它子元素,然后构建新的 MeasuredSpec(size为测量其它子元素得到的最宽值,mode为EXACTLY) 来二次测量之前保存的子元素,再将最终计算的结果设置为自身尺寸。随着嵌套层级的加深会造成测量次数呈指数级增长。

不允许多次测量为了解决多次测量的性能问题,Compose 禁止了多次测量子元素,否则抛出异常 IllegalStateException,使得我们可以进行深层次嵌套而不用担心影响性能。
对应 View 体系中父容器是 wrap_content 需要累加子元素确定自身的情况:首先会要求每个节点对自身进行测量,然后一样是通过递归方式将约束条件 constraints 沿着树向下传递完成所有子元素的测量,根据叶子节点的尺寸和摆放位置向上回溯,进而调整父节点最终确定根节点。简而言之,节点元素会先粗略测量自身,后面再根据叶子元素的位置大小调整自己。

提供固有特性测量

为了解决需要父容器和子元素共同决定尺寸问题并避免多次测量,Compose 加入了固有特性测量(为官方翻译,英文字面有“自身尺寸”的意思),指的是允许父容器在对子元素进行正式测量前,先获得子元素的最大或最小尺寸。
对应 View 体系中父容器是 wrap_content 子元素是 match_parent 的情况:不进行适配的话子元素的大小将直接撑满父容器能获得的最大尺寸。因此 Compose 提供固有特性测量机制(Intrinsic Measurement),用于在正式测量前父容器获取子元素能正常显示的尺寸范围(宽高最大最小值),这样设置了 Modifier.height(IntrinsicSize.Min)  的父容器就能从所有子元素的 minIntrinsicHeight(能正常显示的最小高度)中选出最大的那个,就能得到自身高度并设置给 Modifier.fillMaxHight() 的子元素。

通过从上往下测量(如果存在子节点则测量子节点,测量完子节点后决定自身的尺寸)、从下往上摆放(根据子节点的尺寸摆放子节点)来决定该节点的宽高和坐标。: 

  1. 系统要求根节点 Row 测量自身。
  2. 根节点 Row 要求第一个子元素 Image 测量自身。
  3. 由于 Image 是叶子节点能确定自身的尺寸和摆放并上报。
  4. 根节点 Row 要求第二个子元素 Column 测量自身。
  5. 父容器 Column 要求第一个子元素 Text 测量自身。
  6. 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
  7. 父容器 Column 要求第二个子元素 Text 测量自身。
  8. 由于 Text 是叶子节点能确定自身的尺寸和摆放并上报。
  9. 父容器 Column 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放并上报。
  10. 根节点 Row 所有子元素都测量摆放完毕,可以确定自身的尺寸和摆放。

1.2 自定义用到的核心方法

Modifier.layout() 修饰符用于修改单个可组合项的测量和摆放(会取代父容器原本对它的测量和摆放)。
Layout() 可组合项外层用一个自定义的组合函数包裹使用,界面首次渲染时会将可组合项转化为一个个布局节点 Layout Node。
layout() 方法

布局阶段摆放子元素的入口,在测量完子 Composable 后进行。

二、Modifier.layout() 修饰符

就是自定义一个 Modifier 的扩展函数并返回 Modifier 对象,实现它的 layout( ) 方法如何测量及摆放自身。

fun Modifier.XXX(): Modifier = then(
    layout { measurable, constraints ->
        //测量自身并返回结果
        //通过contraints可以拿到自身宽或高可以设置的最大和最小值
        val placeable = measurable.measure(constraints)
        //获取测量后的控件宽高
        val measuredWidth = placeable.width
        val measuredHeight = placeable.height
        //计算实际宽高
        val needWidth = ...
        val needHeight = ...
        //指定控件宽高
        layout(needWidth, needHeight) {
            //指定摆放的左上角偏移(在xy上移动的距离)
            placeable.placeRelative(0, 0)
        }
    }
)

2.1 例子一

需求:手写一个设置基准线到屏幕的距离,即图中的a。(已有系统API:Modifier.paddingFromBaseline() )

fun Modifier.firstBaseLine2Top(a: Dp): Modifier = then(
    layout { measurable, constraints ->
        //对自身进行测量
        val placeable = measurable.measure(constraints)
        //计算实际宽高
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)    //检查该组件是否支持FirstBaseline(false抛出IllegalStateException )
        val c = placeable[FirstBaseline]    //存在的情况下,获取FirstBaseline离组件顶部的距离
        val b = a.roundToPx() - c    //计算y轴上组件放置位置(x轴直接是0)
        val needHeight = placeable.height + b    //测量的控件高度 + 控件距离顶部的距离
        val needWidth = placeable.width    //宽度直接就是测量出来的
        //设置控件宽高
        layout(needWidth, needHeight) {
            //设置偏移
            placeable.placeRelative(0, b)
        }
    }
)
//使用
@Composable
fun Show() {
    Text(
        text = "Hello Word!",
        modifier = Modifier.firstBaseLine2Top(24.dp)
    )
}

三、Layout() 可组合项

@Composable
inline fun Layout(
    content: @Composable @UiComposable () -> Unit,        //作为该自定义容器的内容
    modifier: Modifier = Modifier,        //外部用来修饰该自定义组件的属性或约束
    measurePolicy: MeasurePolicy        //具体测量和摆放的策略

外层用一个自定义的组合函数包裹后使用。

MeasurePolicy

fun MeasureScope.measure(
    measurables: List<Measurable>,        //集合中的元素对应自己的每一个子元素
    constraints: Constraints        //父容器对自己的布局约束,可以拿到父容器允许自己显示的尺寸最大和最小值,需要的话可以调用copy()修改后再向下传递。
): MeasureResult

重写以实现测量和摆放。

fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int

fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int

fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>, width: Int): Int

重写以实现固有特性测量,例如宽度固定时自身正常显示的高度最大最小值(例如当 Text 宽度固定时,minIntrinsicHeight 为它的高度)。

Measurable

fun measure(constraints: Constraints): Placeable

对子元素进行测量

Constraints

val minWidth: Int
val maxWidth: Int
val minHeight: Int
val maxHeight: Int

可以拿到宽高的最大最小值

MeasureScope

fun layout(
        width: Int,
        height: Int,
        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
        placementBlock: Placeable.PlacementScope.() -> Unit
)

摆放子元素的入口。

PlacementScope

fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f)
fun Placeable.place(x: Int, y: Int, zIndex: Float = 0f)

摆放元素

3.1 测量和摆放

        由测量和摆放两个步骤组成,该顺序使用 DSL 实现,嵌套的方式使得无法摆放未经测量的子元素(MeasurePolicy.measure() 提供测量作用域,layout() 提供摆放作用域) 。

        当一个节点测量子节点的时候会提供 contraints 让子元素了解自己能设置的最大值和最小值,需要修改对子元素的约束可以通过 contraints.copy(minWidth = 20dp) 修改后在子元素测量时传入。

        测量方法执行后会返回一个 placeables 列表,表示子元素已经准备好被摆放了。调用 layout() 在摆放作用域中摆放它们,决定好坐标后调用 place() 摆放。

  1. 重写 MeasurePolicy 的 measure() 方法来自定义布局(测量+摆放)。
  2. 遍历 measurables 拿到每个子元素的测量句柄 measurable,调用 measure() 并传入约束条件 constraints 对子元素进行测量,返回的 placable 对象可以拿到子元素测量后的宽高,就可以累积计算出自身的尺寸。
  3. 调用 layout() 并传入计算出的尺寸摆放自己,会返回一个 MeasureResult 对象用来上报。
  4. 遍历 placables 拿到每个子元素测量后的尺寸,调用 placeRelative() 摆放子元素。
@Composable
fun Demo(
    content: @Composable () -> Unit,    //被包裹的内容
    modifier: Modifier = Modifier    //用于外部修饰自己
) {
    Layout(
        modifier = modifier,
        content = content
    ) {measurables, constraints ->
        //【测量阶段】
        val needHeight = 0
        val needWidth = 0
        //遍历并测量子元素,返回结果集合
        val placeables = measurables.map { measurable ->
            //返回子元素测量结果
            //不需要修改约束就直接将constraints传入
            //需要修改约束就使用 constraints.copy(minWidth = 20dp)
            measurable.measure(constraints).also { placeable ->
                //获取测量后的子元素宽高
                val childMeasuredWidth = placeable.width
                val childMeasuredHeight = placeable.height
                //通过子元素计算自身宽高
                needHeight = ...
                needWidth = ...
                //用集合保存每一行或每一列的宽高信息
            }
        }
        //【摆放阶段】
        layout(needWidth, needHeight) {
            //遍历子元素,设置它们在xy上的偏移
            placeables.forEachIndexed { index, placeable ->
                //通过每一行或每一列的宽高信息,摆放子元素
                val x = ...
                val y = ...
                placeable.placeRelative(x, y)
            }
        }
    }
}

3.2 提供固有特性测量

@Composable
fun Demo(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
){
    Layout(
        modifier = Modifier,
        content = content,
        measurePolicy =  object : MeasurePolicy {
            override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
                //省略...同上方3.1中的代码
            }
            //自身能正常显示的minIntrinsicWidth为所有子元素中最大的那个minIntrinsicWidth
            override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int {
                var maxWidth = 0
                measurables.forEach { intrinsicMeasurable->
                    maxWidth = max(maxWidth, intrinsicMeasurable.minIntrinsicWidth(height))
                }
                return maxWidth
            }
        }
    )
}

四、一些例子

4.1 测量和摆放

4.1.1 例子一(简单)

需求:自定义一个纵向摆放子元素的布局,且尽可能大的占用父容器。

//自定义一个纵向摆放子元素
@Composable
fun MyColumn(
    modifier: Modifier = Modifier,  //用于外部修饰自己
    content: @Composable () -> Unit //接收子元素
){
    //对子元素进行测量和摆放
    Layout(
        modifier = modifier,
        content = content
    ) {measurables, constraints ->
        // 测量每个子元素的尺寸
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        var yPosition = 0   //下一个子元素在y轴上摆放的y坐标
        //摆放子元素
        //尽可能大的占用父布局类似于match_parent(官方Column是尽可能小的占用类似于wrap_content)
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(0, yPosition)
                yPosition += placeable.height
            }
        }
    }
}
//使用
@Composable
fun Show() {
    MyColumn(modifier = Modifier.padding(10.dp)) {
        Text(text = "条目1")
        Text(text = "条目2")
        Text(text = "条目3")
    }
}

3.1.2 例子二(复杂)

需求:横向滑动的瀑布流,可以设置行数。

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,  //用于外部修饰自己
    rows: Int = 3,  //默认显示三行
    content: @Composable () -> Unit //接收子元素
){
    //【第一步:对所有子元素尺寸测量】
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        val rowWidths = IntArray(rows){0}   //记录每一行的宽度
        val rowHeights = IntArray(rows){0}  //记录每一行的高度
        val placeables = measurables.mapIndexed { index, measurable ->
            val placeable = measurable.measure(constraints)
            //根据索引对子元素分组,记录每一行的宽高
            val row = index % rows    //确保只有3行,该值只会得到 0,1,2
            rowWidths[row] += placeable.width    //一行的宽度=这行所有元素宽度之和
            rowHeights[row] = max(rowHeights[row], placeable.height)    //一行的高度=这行最高的元素
            placeable   //测量完要返回placeable对象
        }
        //【第二步:计算自身的尺寸】
        val width = rowWidths.maxOrNull()   //宽度取所有行中宽度最大值
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))  //宽度限制在最大值和最小值之间
            ?: constraints.minWidth //为null就设为最小值
        val height = rowHeights.sumOf { it }    //高度为所有行高之和
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        val rowY = IntArray(rows){0}    //每行子元素在y轴上摆放的坐标
        for (i in 1 until rows) {    //第一行肯定是0,从第二行开始赋值
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]    //当前行y坐标=前一行y坐标+前一行高度
        }
        //【第三步:摆放子元素】
        layout(width, height) {    //在自身的尺寸里摆放
            val rowX = IntArray(rows){0}    //每行子元素在x轴上的坐标
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(rowX[row], rowY[row])
                rowX[row] += placeable.width
            }
        }
    }
}
//使用
val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun Show() {
    StaggeredGrid {
        for (topic in topics) {
            Text(
                text = topic,
                modifier = Modifier.padding(8.dp).background(MaterialTheme.colors.error)
            )
        }
    }
}

4.2 提供固有特性测量 

Compose为提高效率子元素只能测量一次(再次会抛异常)。为了实现预先获得子元素宽高信息再确定自身宽高信息,Compose提供了固有特性测量机制,让我们手动重写方法提供值,使得在子元素正式测量前能获宽高的最大最小值。

4.1 举例一

有一个包含五个 Item 的 Column,需要每个 Item 宽度一致。实际效果是 Item 大小各不相同。

 很容易想到让每个 Item 都占用允许的最大尺寸,但最终导致 Column 扩充为最大尺寸。

对 Column 宽度使用 IntrinsicSize.Max 达到目标效果。正确显示内容所需要的最大宽度的意思。

而如果使用 IntrinsicSize.Min 的效果是这样,Text 的最小固有宽度是每行一个词时的宽度,因此是一个按单词换行的效果。

4.2 举例二

希望分割线 Divider 与最高的 Text 长度相等,发现 Divider 扩展到整个屏幕。Row 会逐个测量子元素,而测得的 Text 高度无法用在限制 Divider。

@Composable
fun WithoutIntrinsics() {
    Row {
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally), text = "Text 1"
        )
        //分割线本意是和左右两个文本一样高
        //Row没有对它的子元素测量做任何限制,填充父容器会尽可能的撑大父容器
        Divider(
            color = Color.Black, modifier = Modifier
                .fillMaxHeight()
                .width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally), text = "Text 2"
        )
    }
}

使用修饰符,可将其子项的高度强行调整为最小固有高度,会递归查询 Row 和子元素的 minIntrinsicHeight。Divider 的 minIntrinsicHeight 为0(即没有给出约束条件它不会占用任何空间),而 Text 在设置了固定宽度的情况下它的 minIntrinsicHeight  即为文本的高度,因此 Row 高度的约束条件将为 Text 的最大高度

//方式一:官方API
@Composable
fun WithIntrinsics(){
    //高度设为最小(刚好包裹子元素)
    Row(modifier = Modifier.height(IntrinsicSize.Min)) {
        //其他代码未变
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值