Google 致力于为黑人社区推动种族平等。查看具体举措

Compose 中的布局

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

可组合函数是 Compose 的基本构建块。可组合函数是用于描述界面中某一部分的函数。该函数接受一些输入并生成屏幕上显示的内容。如需详细了解可组合项,请参阅 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(verticalGravity = Alignment.CenterVertically) {
        Image(...)
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

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

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

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

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

上述每个基本布局都定义了自己的重心设置,从而指定了应如何排列相应的元素。如需配置这些元素,请使用修饰符。

修饰符

借助修饰符,您可以略微调整可组合项的呈现方式。您可以使用修饰符来执行以下操作:

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

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

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

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

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

  • clickable() 使可组合项响应用户输入。
  • padding() 在元素周围留出空间。
  • fillMaxWidth() 使可组合项填充其父项给它的最大宽度。
  • preferredSize() 指定元素的首选宽度和高度。

修饰符函数的顺序非常重要。由于每个函数都会对上一个函数返回的 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
    }
}

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

可滚动布局

使用 ScrollableRowScrollableColumn 可使 RowColumn 内的元素滚动。

@Composable
fun Feed(
  feedItems: List<Artist>,
  onSelected: (Artist) -> Unit
) {
  ScrollableColumn(Modifier.fillMaxSize()) {
    feedItems.forEach {
      ArtistCard(it, onSelected(it))
    }
  }
}

可滚动列中的几个类似布局

如果要显示的元素很少,这种方法效果很好,但对于大型数据集,很快就会出现性能问题。如需仅显示屏幕上可见的部分元素,请使用 LazyColumnForLazyRowFor

@Composable
fun Feed(
  feedItems: List<Artist>,
  onSelected: (Artist) -> Unit
) {
  Surface(Modifier.fillMaxSize()) {
    LazyColumnFor(feedItems) { item ->
      ArtistCard(it, onSelected(it))
    }
  }
}

内置 Material 组件

Compose 最高级别的界面抽象语言是 Material Design。Compose 提供了大量开箱即用的可组合项来简化界面的构建。诸如 DrawerFloatingActionButtonTopAppBar 之类的元素都有提供。

Material 组件大量使用插槽 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。插槽会在界面中留出空白区域,让开发者按照自己的意愿来填充。例如,下面是您可以在 TopAppBar 中自定义的插槽:

显示了应用栏中的插槽,您可以在其中添加界面元素

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

Material 支持的最高级别的可组合项是 ScaffoldScaffold 可让您实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶级 Material 组件(如 TopAppBarBottomAppBarFloatingActionButtonDrawer)提供插槽。通过使用 Scaffold,很容易确保这些组件得到适当放置且正确地协同工作。

显示了一个使用 Scaffold 以与 Material Design 一致的方式排列元素的布局

@Composable
fun HomeScreen(...) {
    Scaffold (
        drawerContent = { ... },
        topBar = { ... },
        bodyContent = { ... }
    )
}

ConstraintLayout

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

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. 使用 tag 修饰符将在 ConstraintSet 中创建的引用分配给可组合项。
@Composable
fun DecoupledConstraintLayout() {
    WithConstraints {
        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.tag("button")
            ) {
                Text("Button")
            }

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

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

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

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

自定义布局

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

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

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

使用布局修饰符

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

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

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

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

生成该间距的代码如下:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = Modifier.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.toIntPx() - 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.placeRelative(x, y) 将子项放置在屏幕上。如果未放置子项,它们将不可见。y 位置对应于上内边距,即文本的第一条基线的位置。placeRelative 在从右到左的上下文中自动镜像位置。

如需验证这段代码是否可以发挥预期的作用,请在 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 MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

layout 修饰符类似,measurables 是需要测量的子项的列表,而 constraints 是传递给 Layout 的约束条件。按照与前面相同的逻辑,可按如下方式实现 MyOwnColumn

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    children: @Composable() () -> Unit
) {
    Layout(
        modifier = modifier,
        children = children
    ) { 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 约束条件的约束,它们的放置基于前一个可组合项的 yPosition

该自定义可组合项的使用方式如下:

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

alt_text

布局方向

您可以使用 LayoutDirection 上下文环境来更改可组合项的布局方向。

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

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

了解详情

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