Compose 中的布局

Jetpack Compose 可让您更轻松地设计和构建应用的界面。本文档介绍了 Compose 为了帮助您布置界面元素而提供的一些构建块,并向您展示了如何在需要时构建更专业的布局。

Compose 中布局的目标

布局系统的 Jetpack Compose 实现有两个主要目标:一是实现高性能,二是让开发者能够轻松编写自定义布局。在 Compose 中,通过避免多次测量布局子级可实现高性能。如果需要进行多次测量,Compose 具有一个特殊系统,即固有特性测量。如需详细了解此功能,请参阅固有特性测量部分

可组合函数的基础知识

可组合函数是 Compose 的基本构建块。可组合函数是一种发出 Unit 的函数,用于描述界面中的某一部分。该函数接受一些输入并生成屏幕上显示的内容。如需详细了解可组合项,请参阅 Compose 构思模型文档。

一个可组合函数可能会发出多个界面元素。不过,如果您未提供有关如何排列这些元素的指导,Compose 可能会以您不喜欢的方式排列它们。例如,以下代码会生成两个文本元素:

@Composable
fun ArtistCard() {
    Text("Alfred Sisley")
    Text("3 minutes ago")
}

如果您未提供有关如何排列这两个文本元素的指导,Compose 会将它们堆叠在一起,使其无法阅读:

两个文本元素相互叠加,使文本无法阅读

Compose 提供了一系列现成可用的布局来帮助您排列界面元素,并且可让您轻松地定义您自己的更专业的布局。

标准布局组件

在许多情况下,您只需使用 Compose 的标准布局元素即可。

使用 Column 可将多个项垂直地放置在屏幕上。

@Composable
fun ArtistCard() {
    Column {
        Text("Alfred Sisley")
        Text("3 minutes ago")
    }
}

两个文本元素按列布局排列,因此文本清晰易读

同样,使用 Row 可将多个项水平地放置在屏幕上。ColumnRow 都支持配置它们所含元素的对齐方式。

@Composable
fun ArtistCard(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(/*...*/)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

示例显示了一个更复杂的布局,一列文本元素旁边有一个小图形

使用 Box 可将一个元素放在另一个元素上。

比较三个简单的布局可组合项:Column、Row 和 Box

通常,您只需要这些构建块。您可以自行编写可组合函数,将这些布局组合成更精美的布局,以适合您的应用。

如需在 Row 中设置子项的位置,请设置 horizontalArrangementverticalAlignment 参数。对于 Column,请设置 verticalArrangementhorizontalAlignment 参数:

@Composable
fun ArtistCard(artist: Artist) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.End
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

内容项靠右对齐

修饰符

借助修饰符,您可以修饰或扩充可组合项。您可以使用修饰符来执行以下操作:

  • 更改可组合项的大小、布局、行为和外观
  • 添加信息,如无障碍标签
  • 处理用户输入
  • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放

修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier 类函数来创建修饰符。您可以将以下函数连在一起以将其组合起来:

@Composable
fun ArtistCard(
    artist: Artist,
    onClick: () -> Unit
) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
        Spacer(Modifier.size(padding))
        Card(elevation = 4.dp) { /*...*/ }
    }
}

一个更复杂的布局,使用修饰符来更改图形的排列方式,以及哪些区域响应用户输入

请注意,在上面的代码中,结合使用了不同的修饰符函数。

  • clickable 使可组合项响应用户输入,并显示涟漪。
  • padding 在元素周围留出空间。
  • fillMaxWidth 使可组合项填充其父项为它提供的最大宽度。
  • size() 指定元素的首选宽度和高度。

修饰符顺序很重要

修饰符函数的顺序非常重要。由于每个函数都会对上一个函数返回的 Modifier 进行更改,因此顺序会影响最终结果。让我们来看看这方面的一个示例:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

整个区域(包括围绕边缘的内边距)都响应点击操作

在上面的代码中,整个区域(包括周围的内边距)都是可点击的,因为 padding 修饰符应用在 clickable 修饰符后面。如果修饰符顺序颠倒,由 padding 添加的空间就不会响应用户输入:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .padding(padding)
            .clickable(onClick = onClick)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}

围绕布局边缘的内边距不再响应点击操作

内置修饰符

Jetpack Compose 提供了一个内置修饰符列表,可帮助您修饰或扩充可组合项。已引入 paddingclickablefillMaxWidth 等修饰符。下面列出了其他常用修饰符:

size

默认情况下,Compose 中提供的布局会封装其子项。但是,您可以使用 size 修饰符设置尺寸:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(/*...*/)
        Column { /*...*/ }
    }
}

请注意,如果指定的尺寸不符合来自布局父项的约束条件,则可能不会采用该尺寸。如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件,请使用 requiredSize 修饰符:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

子图片的尺寸大于其父项的约束条件

在此示例中,即使父项的 height 设置为 100.dpImage 的高度还是 150.dp,因为 requiredSize 修饰符优先级较高。

如果您希望子布局填充父项允许的所有可用高度,请添加 fillMaxHeight 修饰符(Compose 还提供了 fillMaxSizefillMaxWidth):

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.fillMaxHeight()
        )
        Column { /*...*/ }
    }
}

图片高度与其父项相同

如需在文本基线上方添加内边距,以实现从布局顶部到基线保持特定距离,请使用 paddingFromBaseline 修饰符:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(
                text = artist.name,
                modifier = Modifier.paddingFromBaseline(top = 50.dp)
            )
            Text(artist.lastSeenOnline)
        }
    }
}

上方有内边距的文本

偏移量

如需相对于原始位置放置布局,请添加 offset 修饰符,并设置在 x 轴和 y 轴的偏移量。偏移量可以是正数,也可以是非正数。paddingoffset 之间的区别在于,向可组合项添加 offset 不会改变其测量结果:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

文本移至其父容器的右侧

offset 修饰符根据布局方向水平应用。在从左到右的上下文中,如果 offset 为正值,会将元素向右移,而在从右到左的上下文中,则会将元素向左移。如果您需要设置偏移量,而不考虑布局方向,请参阅 absoluteOffset 修饰符,其中,正偏移值始终将元素向右移。

Compose 中的类型安全

在 Compose 中,有些修饰符仅适用于某些可组合项的子项。例如,如果您希望使某个子项与父项 Box 同样大,而不影响 Box 尺寸,请使用 matchParentSize 修饰符。

Compose 通过自定义作用域强制实施此类型安全机制。例如,matchParentSize 仅在 BoxScope 中可用。因此,仅当在 Box 中使用子项时,才能使用此修饰符。

限定作用域的修饰符会将父项应知晓的关于子项的一些信息告知父项。这些修饰符通常称为“父项数据修饰符”。它们的内部构件与通用修饰符不同,但从使用角度来看,这些差异并不重要。

Box 中的 matchParentSize

如上所述,如果您希望子布局与父项 Box 尺寸相同而不影响 Box 的尺寸,请使用 matchParentSize 修饰符。

请注意,matchParentSize 仅在 Box 作用域内可用,这意味着它仅适用于 Box 可组合项的直接子项

在以下示例中,子项 Spacer 从其父项 Box 获取自己的尺寸,在这种情况下,后者又会从其最大的子项 ArtistCard 中获取自己的尺寸。

@Composable
fun MatchParentSizeComposable() {
    Box {
        Spacer(Modifier.matchParentSize().background(Color.LightGray))
        ArtistCard()
    }
}

填充容器的灰色背景

如果使用 fillMaxSize 代替 matchParentSizeSpacer 将占用父项允许的所有可用空间,反过来使父项展开并填满所有可用空间。

填充屏幕的灰色背景

Row 和 Column 中的 weight

如前文的内边距和尺寸部分所述,默认情况下,可组合项的尺寸由其封装的内容定义。您可以使用仅可在 RowScopeColumnScope 中使用的 weight 修饰符,将可组合项的尺寸设置为可在其父项内灵活调整。

让我们以包含两个 Box 可组合项的 Row 为例。第一个框的 weight 是第二个框的两倍,因此其宽度也相差两倍。由于 Row 的宽度为 210.dp,因此第一个 Box 的宽度为 140.dp,第二个的宽度为 70.dp

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.fillMaxWidth()
    ) {
        Image(
            /*...*/
            modifier = Modifier.weight(2f)
        )
        Column(
            modifier = Modifier.weight(1f)
        ) {
            /*...*/
        }
    }
}

图片宽度是文字宽度的两倍

可滚动布局

如需详细了解可滚动布局,请参阅 Compose 手势文档。

如需了解列表和延迟列表,请参阅 Compose 列表文档

自适应布局

在设计布局时,应考虑不同的屏幕方向和设备类型尺寸。Compose 提供了一些开箱即用的机制,可帮助您根据各种屏幕配置调整可组合项的布局。

约束条件

如需了解来自父项的约束条件并相应地设计布局,您可以使用 BoxWithConstraints。您可以在内容 lambda 的作用域内找到测量约束条件。您可以使用这些测量约束条件,为不同的屏幕配置组成不同的布局:

@Composable
fun WithConstraintsComposable() {
    BoxWithConstraints {
        Text("My minHeight is $minHeight while my maxWidth is $maxWidth")
    }
}

基于槽位的布局

Compose 提供了大量基于 Material Design 的可组合项以及 androidx.compose.material:material 依赖项(在 Android Studio 中创建 Compose 项目时提供),旨在简化界面的构建。诸如 DrawerFloatingActionButtonTopAppBar 之类的元素都有提供。

Material 组件大量使用槽位 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。这种方法使组件变得更加灵活,因为它们接受可以自行配置的子元素,而不必公开子元素的每个配置参数。槽位会在界面中留出空白区域,让开发者按照自己的意愿来填充。例如,下面是您可以在 TopAppBar 中自定义的槽位:

显示 Material Components 应用栏中的可用槽位的图表

可组合项通常采用 content 可组合 lambda (content: @Composable () -> Unit)。槽位 API 会针对特定用途公开多个 content 参数。例如,TopAppBar 可让您为 titlenavigationIconactions 提供内容。

例如,Scaffold 可让您实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶级 Material 组件(如 TopAppBarBottomAppBarFloatingActionButtonDrawer)提供槽位。通过使用 Scaffold,可轻松确保这些组件得到适当放置且正确地协同工作。

JetNews 示例应用,该应用使用 Scaffold 确定多个元素的位置

@Composable
fun HomeScreen(/*...*/) {
    Scaffold(
        drawerContent = { /*...*/ },
        topBar = { /*...*/ },
        content = { /*...*/ }
    )
}

ConstraintLayout

ConstraintLayout 有助于根据可组合项的相对位置将它们放置在屏幕上,它是使用多个嵌套 RowColumnBox 和自定义布局元素的替代方案。在实现对齐要求比较复杂的较大布局时,ConstraintLayout 很有用。

如需使用 Compose 中的 ConstraintLayout,您需要在 build.gradle 中添加以下依赖项:

implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08"

Compose 中的 ConstraintLayout 支持 DSL

  • 引用是使用 createRefs()createRefFor() 创建的,ConstraintLayout 中的每个可组合项都需要有与之关联的引用。
  • 约束条件是使用 constrainAs() 修饰符提供的,该修饰符将引用作为参数,可让您在主体 lambda 中指定其约束条件。
  • 约束条件是使用 linkTo() 或其他有用的方法指定的。
  • parent 是一个现有的引用,可用于指定对 ConstraintLayout 可组合项本身的约束条件。

下面是使用 ConstraintLayout 的可组合项的示例:

@Composable
fun ConstraintLayoutContent() {
    ConstraintLayout {
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = { /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

此代码使用 16.dp 的外边距来约束 Button 顶部到父项的距离,同样使用 16.dp 的外边距来约束 TextButton 底部的距离。

显示了按 ConstraintLayout 排列的按钮和文本元素

如需查看有关如何使用 ConstraintLayout 的更多示例,请参考布局 Codelab

Decoupled API

ConstraintLayout 示例中,约束条件是在应用它们的可组合项中使用修饰符以内嵌方式指定的。不过,在某些情况下,最好将约束条件与应用它们的布局分离开来。例如,您可能会希望根据屏幕配置来更改约束条件,或在两个约束条件集之间添加动画效果。

对于此类情况,您可以通过不同的方式使用 ConstraintLayout

  1. ConstraintSet 作为参数传递给 ConstraintLayout
  2. 使用 layoutId 修饰符将在 ConstraintSet 中创建的引用分配给可组合项。
@Composable
fun DecoupledConstraintLayout() {
    BoxWithConstraints {
        val constraints = if (minWidth < 600.dp) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }

        ConstraintLayout(constraints) {
            Button(
                onClick = { /* Do something */ },
                modifier = Modifier.layoutId("button")
            ) {
                Text("Button")
            }

            Text("Text", Modifier.layoutId("text"))
        }
    }
}

private fun decoupledConstraints(margin: Dp): ConstraintSet {
    return ConstraintSet {
        val button = createRefFor("button")
        val text = createRefFor("text")

        constrain(button) {
            top.linkTo(parent.top, margin = margin)
        }
        constrain(text) {
            top.linkTo(button.bottom, margin)
        }
    }
}

然后,当您需要更改约束条件时,只需传递不同的 ConstraintSet 即可。

了解详情

如需详细了解 Compose 中的 ConstraintLayout,请参阅 Jetpack Compose Codelab 中的布局的约束布局部分,以及使用 ConstraintLayout 的 Compose 示例中的 API 实际运用。

自定义布局

在 Compose 中,界面元素由可组合函数表示,此类函数在被调用后会发出一部分界面,这部分界面随后会被添加到呈现在屏幕上的界面树中。每个界面元素都有一个父元素,还可能有多个子元素。此外,每个元素在其父元素中都有一个位置,指定为 (x, y) 位置;也都有一个尺寸,指定为 widthheight

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

单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。如果某个元素测量了它的子元素两次,而该子元素又测量了它的一个子元素两次,依此类推,那么一次尝试布置整个界面就不得不做大量的工作,这将很难让应用保持良好的性能。不过,有时除了子元素的单遍测量告知您的信息之外,您确实还需要一些额外的信息。有一些方法可以有效地处理这样的情况,这些方法在固有特性测量部分进行了介绍。

作用域的使用决定了您可以衡量和放置子项的时间。只能在测量和布局传递期间测量布局,并且只能在布局传递期间以及在事前测量之后才能放置子项。由于 Compose 作用域(如 MeasureScopePlacementScope),此操作在编译时强制执行。

使用布局修饰符

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

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

假设我们在屏幕上显示 Text,并控制从顶部到第一行文本基线的距离。这正是 paddingFromBaseline 修饰符的作用,我们在这里将其作为一个示例来实现。为此,请使用 layout 修饰符将可组合项手动放置在屏幕上。Text 上内边距设为 24.dp 时的预期行为如下:

显示了正常界面内边距与文本内边距之间的差异,前者设置元素的间距,后者设置从一条基线到下一条基线的间距

生成该间距的代码如下:

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) 来测量以可测量参数表示的 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))
    }
}

文本元素的多种预览;一种显示元素之间的普通内边距,另一种显示从一条基线到下一条基线的内边距

创建自定义布局

layout 修饰符仅更改调用可组合项。如需测量和布置多个可组合项,请改用 Layout 可组合项。此可组合项允许您手动测量和布置子项。ColumnRow 等所有较高级别的布局都使用 Layout 可组合项构建而成。

我们来构建一个非常基本的 Column。大多数自定义布局都遵循以下模式:

@Composable
fun MyBasicColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        children = 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!")
    }
}

几个文本元素依次堆叠成一列。

自定义布局的实际运用

如需详细了解自定义布局和修饰符,请参阅 Jetpack Compose 中的布局 Codelab,然后查看自定义布局的 Compose 示例中的 API 实际运用。

布局方向

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

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

使用 layoutDirection 时,应使用 place 放置可组合项。与 placeRelative 方法不同,place 不会根据布局方向(从左到右与从右到左)发生变化。

固有特性测量

Compose 有一项规则,即,子项只能测量一次,测量两次就会引发运行时异常。但是,有时需要先收集一些关于子项的信息,然后再测量子项。

借助固有特性,您可以先查询子项,然后再进行实际测量。

对于可组合项,您可以查询其 intrinsicWidthintrinsicHeight

  • (min|max)IntrinsicWidth:给定此高度,可以正确绘制内容的最小/最大宽度是多少?
  • (min|max)IntrinsicHeight:给定此宽度,可以正确绘制内容的最小/最大高度是多少?

例如,如果您查询具有无限 widthTextminIntrinsicHeight,它将返回 Textheight,就好像该文本是在单行中绘制的一样。

固有特性的实际运用

假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本,如下所示:

两个文本元素并排显示,中间用垂直分隔线隔开

我们该怎么做?我们可以将两个 Text 放在同一 Row,并在其中最大程度地扩展,另外在中间放置一个 Divider。我们需要将分隔线的高度设置为与最高的 Text 相同,粗细设置为 width = 1.dp

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )

        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

@Preview
@Composable
fun TwoTextsPreview() {
    MaterialTheme {
        Surface {
            TwoTexts(text1 = "Hi", text2 = "there")
        }
    }
}

预览时,我们发现分隔线扩展到整个屏幕,这并不是我们想要的效果:

两个文本元素并排显示,中间用分隔线隔开,但分隔线向下延伸到文本底部下方

之所以出现这种情况,是因为 Row 会逐个测量每个子项,并且 Text 的高度不能用于限制 Divider。我们希望 Divider 以一个给定的高度来填充可用空间。为此,我们可以使用 height(IntrinsicSize.Min) 修饰符。

height(IntrinsicSize.Min) 可将其子项的高度强行调整为最小固有高度。由于该修饰符具有递归性,因此它将查询 Row 及其子项 minIntrinsicHeight

将其应用到代码中,就能达到预期的效果:

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier.fillMaxHeight().width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}

预览如下:

两个文本元素并排显示,中间用垂直分隔线隔开

Row 可组合项的 minIntrinsicHeight 将作为其子项的最大 minIntrinsicHeightDivider element'sminIntrinsicHeight 为 0,因为如果没有给出约束条件,它不会占用任何空间;如果给出特定 widthText minIntrinsicHeight 将为文本的高度。因此,Row 元素的 height 约束条件将为 Text 的最大 minIntrinsicHeight。而 Divider 会将其 height 扩展为 Row 给定的 height 约束条件。

自定义布局中的固有特性

创建自定义 Layoutlayout 修饰符时,系统会根据近似值自动计算固有测量结果。因此,计算结果可能并不适用于所有布局。这些 API 提供了替换这些默认值的选项。

要指定自定义 Layout 的固有特性测量,则在创建该布局时替换 MeasurePolicyminIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight

@Composable
fun MyCustomComposable(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    return object : MeasurePolicy {
        override fun MeasureScope.measure(
            measurables: List<Measurable>,
            constraints: Constraints
        ): MeasureResult {
            // Measure and layout here
        }

        override fun IntrinsicMeasureScope.minIntrinsicWidth(
            measurables: List<IntrinsicMeasurable>,
            height: Int
        ) = {
            // Logic here
        }

        // Other intrinsics related methods have a default value,
        // you can override only the methods that you need.
    }
}

创建自定义 layout 修饰符时,替换 LayoutModifier 界面中的相关方法。

fun Modifier.myCustomModifier(/* ... */) = this.then(object : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // Measure and layout here
    }

    override fun IntrinsicMeasureScope.minIntrinsicWidth(
        measurable: IntrinsicMeasurable,
        height: Int
    ): Int = {
        // Logic here
    }

    // Other intrinsics related methods have a default value,
    // you can override only the methods that you need.
})

了解详情

如需详细了解固有特性衡量,请参阅 Jetpack Compose 中的布局 Codelab 中的固有特性部分。

了解详情

如需了解详情,请参阅 Jetpack Compose 中的布局 Codelab