自定义布局

在 Compose 中,UI 元素由可组合函数表示,这些函数在被调用时会发出 UI 片段,然后将其添加到 UI 树中,并在屏幕上呈现。每个 UI 元素都有一个父元素和潜在的多个子元素。每个元素也位于其父元素内,指定为 (x, y) 位置,以及一个大小,指定为 widthheight

父元素定义其子元素的约束。要求元素在其约束内定义其大小。约束限制元素的最小和最大 widthheight。如果元素有子元素,它可能会测量每个子元素以帮助确定其大小。一旦元素确定并报告其自身的大小,它就有机会定义如何相对于自身放置其子元素,如 创建自定义布局 中详细描述。

在 UI 树中布局每个节点是一个包含三个步骤的过程。每个节点必须

  1. 测量任何子元素
  2. 确定自身的大小
  3. 放置其子元素

Three steps of node layout: measure children, decide size, place children

作用域的使用定义了何时可以测量和放置您的子元素。测量布局只能在测量和布局阶段进行,并且子元素只能在布局阶段放置(并且仅在测量后)。由于 Compose 作用域(如 MeasureScopePlacementScope),这在编译时得到执行。

使用布局修饰符

您可以使用 layout 修饰符来修改元素的测量和布局方式。Layout 是一个 lambda;其参数包括您可以测量的元素(作为 measurable 传递)以及该可组合的传入约束(作为 constraints 传递)。自定义布局修饰符可能如下所示

fun Modifier.customLayoutModifier() =
    layout { measurable, constraints ->
        // ...
    }

让我们在屏幕上显示一个 Text 并控制从顶部到文本第一行基线的距离。这正是 paddingFromBaseline 修饰符的作用,我们在这里将其作为示例进行实现。为此,请使用 layout 修饰符手动将可组合项放置在屏幕上。以下是所需的 Text 顶部填充设置为 24.dp 的行为

Shows the difference between normal UI padding, which sets the space between elements, and text padding that sets the space from one baseline to the next

以下是生成该间距的代码

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = layout { measurable, constraints ->
    // Measure the composable
    val placeable = measurable.measure(constraints)

    // Check the composable has a first baseline
    check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
    val firstBaseline = placeable[FirstBaseline]

    // Height of the composable with padding - first baseline
    val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
    val height = placeable.height + placeableY
    layout(placeable.width, height) {
        // Where the composable gets placed
        placeable.placeRelative(0, placeableY)
    }
}

以下是该代码中的内容

  1. measurable lambda 参数中,您可以通过调用 measurable.measure(constraints) 来测量由 measurable 参数表示的 Text
  2. 您可以通过调用 layout(width, height) 方法指定可组合项的大小,该方法还提供了一个用于放置包装元素的 lambda。在这种情况下,它是最后一个基线和添加的顶部填充之间的高度。
  3. 您可以通过调用 placeable.place(x, y) 在屏幕上定位包装元素。如果未放置包装元素,则它们将不可见。y 位置对应于顶部填充 - 文本第一行基线的位置。

要验证这是否按预期工作,请在 Text 上使用此修饰符

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
    }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
    MyApplicationTheme {
        Text("Hi there!", Modifier.padding(top = 32.dp))
    }
}

Multiple previews of text elements; one shows ordinary padding between elements, the other shows padding from one baseline to the next

创建自定义布局

layout 修饰符仅更改调用可组合项。要测量和布局多个可组合项,请改用 Layout 可组合项。此可组合项允许您手动测量和布局子元素。所有更高级别的布局(如 ColumnRow)都是使用 Layout 可组合项构建的。

让我们构建一个非常基本的 Column 版本。大多数自定义布局都遵循此模式

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
        // ...
    }
}

layout 修饰符类似,measurables 是需要测量的子元素列表,constraints 是来自父元素的约束。遵循与之前相同的逻辑,MyBasicColumn 可以这样实现

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each children
            measurable.measure(constraints)
        }

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Track the y co-ord we have placed children up to
            var yPosition = 0

            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // Record the y co-ord placed up to
                yPosition += placeable.height
            }
        }
    }
}

子可组合项受 Layout 约束(无 minHeight 约束)约束,并且根据前一个可组合项的 yPosition 进行放置。

以下是此自定义可组合项的使用方式

@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
    MyBasicColumn(modifier.padding(8.dp)) {
        Text("MyBasicColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

Several text elements stacked one above the next in a column.

布局方向

通过更改 LocalLayoutDirection 组合局部来更改可组合项的布局方向。

如果您正在手动将可组合项放置在屏幕上,则 LayoutDirectionlayout 修饰符或 Layout 可组合项的 LayoutScope 的一部分。

使用 layoutDirection 时,请使用 place 放置可组合项。与 placeRelative 方法不同,place 不会根据布局方向(从左到右与从右到左)更改。

自定义布局实战

Compose 中的基本布局 中了解有关布局和修饰符的更多信息,并在 创建自定义布局的 Compose 示例 中查看自定义布局的实际应用。

了解更多

要了解有关 Compose 中自定义布局的更多信息,请查阅以下其他资源。

视频