Compose 布局基础知识

Jetpack Compose 使设计和构建应用的 UI 变得更加容易。Compose 通过以下方式将状态转换为 UI 元素:

  1. 元素组合
  2. 元素布局
  3. 元素绘制

Compose transforming state to UI via composition, layout, drawing

本文档重点介绍元素的布局,解释了 Compose 提供的一些构建块,以帮助您布置 UI 元素。

Compose 中布局的目标

Jetpack Compose 布局系统实现有两个主要目标

可组合函数的基础知识

可组合函数是 Compose 的基本构建块。可组合函数是一个发出 Unit 的函数,它描述了 UI 的一部分。该函数接收一些输入并生成屏幕上显示的内容。有关可组合项的更多信息,请查看Compose 心智模型 文档。

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

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

在没有关于您希望如何排列它们的指导的情况下,Compose 将文本元素堆叠在一起,使它们难以阅读

Two text elements drawn on top of each other, making the text unreadable

Compose 提供了一组现成的布局来帮助您排列 UI 元素,并使定义您自己的更专业的布局变得容易。

标准布局组件

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

使用Column 在屏幕上垂直放置项目。

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

Two text elements arranged in a column layout, so the text is readable

类似地,使用Row 在屏幕上水平放置项目。ColumnRow 都支持配置其包含元素的对齐方式。

@Composable
fun ArtistCardRow(artist: Artist) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Column {
            Text(artist.name)
            Text(artist.lastSeenOnline)
        }
    }
}

Shows a more complex layout, with a small graphic next to a column of text elements

使用Box 将元素放置在另一个元素的顶部。Box 还支持配置其包含元素的特定对齐方式。

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(bitmap = artist.image, contentDescription = "Artist image")
        Icon(Icons.Filled.Check, contentDescription = "Check mark")
    }
}

Shows two elements stacked on one another

通常,这些构建块就是您所需要的一切。您可以编写自己的可组合函数,将这些布局组合成更复杂的布局以适合您的应用。

Compares three simple layout composables: column, row, and box

要在 Row 中设置子级的位置,请设置 horizontalArrangementverticalAlignment 参数。对于 Column,请设置 verticalArrangementhorizontalAlignment 参数

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

Items are aligned to the right

布局模型

在布局模型中,UI 树在一遍扫描中布局。首先要求每个节点测量自身,然后递归地测量任何子节点,将大小约束向下传递到树中的子节点。然后,对叶节点进行大小调整和放置,并将已解析的大小和放置指令向上传递回树。

简而言之,父节点在子节点之前进行测量,但在子节点之后进行大小调整和放置。

考虑以下 SearchResult 函数。

@Composable
fun SearchResult() {
    Row {
        Image(
            // ...
        )
        Column {
            Text(
                // ...
            )
            Text(
                // ...
            )
        }
    }
}

此函数生成以下 UI 树。

SearchResult
  Row
    Image
    Column
      Text
      Text

SearchResult 示例中,UI 树布局遵循以下顺序

  1. 要求根节点 Row 进行测量。
  2. 根节点 Row 要求其第一个子节点 Image 进行测量。
  3. Image 是叶节点(即它没有子节点),因此它报告大小并返回放置指令。
  4. 根节点 Row 要求其第二个子节点 Column 进行测量。
  5. Column 节点要求其第一个 Text 子节点进行测量。
  6. 第一个 Text 节点是叶节点,因此它报告大小并返回放置指令。
  7. Column 节点要求其第二个 Text 子节点进行测量。
  8. 第二个 Text 节点是叶节点,因此它报告大小并返回放置指令。
  9. 现在 Column 节点已测量、调整大小并放置了其子节点,它可以确定自身的大小和位置。
  10. 现在根节点 Row 已测量、调整大小并放置了其子节点,它可以确定自身的大小和位置。

Ordering of measuring, sizing, and placement in Search Result UI tree

性能

Compose 通过仅测量子节点一次来实现高性能。单遍测量有利于性能,使 Compose 能够高效地处理深层 UI 树。如果一个元素对其子节点进行两次测量,并且该子节点对其每个子节点进行两次测量,依此类推,则尝试布局整个 UI 将需要执行大量工作,这使得难以保持应用的性能。

如果您的布局出于某种原因需要多次测量,Compose 提供了一个特殊的系统,即内在测量。您可以在Compose 布局中的内在测量中阅读有关此功能的更多信息。

由于测量和放置是布局传递的不同子阶段,因此仅影响项目放置(而不是测量)的任何更改都可以单独执行。

在布局中使用修饰符

Compose 修饰符中所述,您可以使用修饰符来装饰或增强可组合项。修饰符对于自定义布局至关重要。例如,这里我们链接了几个修饰符来自定义 ArtistCard

@Composable
fun ArtistCardModifiers(
    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 = CardDefaults.cardElevation(defaultElevation = 4.dp),
        ) { /*...*/ }
    }
}

A still more complex layout, using modifiers to change how the graphics are arranged and which areas respond to user input

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

  • clickable 使可组合项对用户输入做出反应并显示波纹。
  • padding 在元素周围留出空间。
  • fillMaxWidth 使可组合项填充其父级给定的最大宽度。
  • size() 指定元素的首选宽度和高度。

可滚动布局

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 项目时包含)简化 UI 构建。它提供了诸如 DrawerFloatingActionButtonTopAppBar 等元素。

Material 组件大量使用插槽 API,这是 Compose 引入的一种模式,用于在可组合函数之上添加一层自定义功能。这种方法使组件更加灵活,因为它们接受一个子元素来配置自身,而不是必须公开子元素的每个配置参数。插槽在 UI 中留出一个空位,供开发者根据需要填充。例如,以下是在 TopAppBar 中可以自定义的插槽。

A diagram showing the available slots in a Material Components app bar

可组合函数通常接受一个 content 可组合 lambda(content: @Composable () -> Unit)。插槽 API 为特定用途公开了多个 content 参数。例如,TopAppBar 允许您提供 titlenavigationIconactions 的内容。

例如,Scaffold 允许您使用基本的 Material Design 布局结构实现 UI。Scaffold 为最常见的顶级 Material 组件提供插槽,例如 TopAppBarBottomAppBarFloatingActionButtonDrawer。通过使用 Scaffold,可以轻松确保这些组件正确放置并协同工作。

The JetNews sample app, which uses Scaffold to position multiple elements

@Composable
fun HomeScreen(/*...*/) {
    ModalNavigationDrawer(drawerContent = { /* ... */ }) {
        Scaffold(
            topBar = { /*...*/ }
        ) { contentPadding ->
            // ...
        }
    }
}