Compose 布局基础知识

Jetpack Compose 可让您更轻松地设计和构建应用的界面。Compose 通过以下方法将状态转换为界面元素:

  1. 元素的组合
  2. 元素的布局
  3. 元素的绘图

Compose 通过组合、布局、绘图将状态转换为界面

本文重点介绍元素的布局,并介绍了 Compose 为构建界面元素提供的一些构建块。

Compose 中布局的目标

布局系统的 Jetpack 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 可将元素放在其他元素上。Box 还支持为其包含的元素配置特定的对齐方式。

@Composable
fun ArtistAvatar(artist: Artist) {
    Box {
        Image(/*...*/)
        Icon(/*...*/)
    }
}

图中显示两个元素堆叠在一起

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

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

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

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

内容项靠右对齐

布局模型

在布局模型中,通过单次传递即可完成界面树布局。首先,系统会要求每个节点对自身进行测量,然后以递归方式完成所有子节点的测量,并将尺寸约束条件沿着树向下传递给子节点。再后,确定叶节点的尺寸和放置位置,并将经过解析的尺寸和放置指令沿着树向上回传。

简而言之,父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。

请参考以下 SearchResult 函数。

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

此函数会生成以下界面树。

SearchResult
  Row
    Image
    Column
      Text
      Text

SearchResult 示例中,界面树布局遵循以下顺序:

  1. 系统要求根节点 Row 对自身进行测量。
  2. 根节点 Row 要求其第一个子节点(即 Image)进行测量。
  3. Image 是一个叶节点(也就是说,它没有子节点),因此该节点会报告尺寸并返回放置指令。
  4. 根节点 Row 要求其第二个子节点(即 Column)进行测量。
  5. 节点 Column 要求其第一个子节点 Text 进行测量。
  6. 由于第一个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令。
  7. 节点 Column 要求其第二个子节点 Text 进行测量。
  8. 由于第二个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令。
  9. 现在,节点 Column 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了。
  10. 现在,根节点 Row 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了。

在搜索结果界面树中进行测量以及确定尺寸和放置位置的顺序

性能

Compose 通过只测量一次子项来实现高性能。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。如果某个元素测量了它的子元素两次,而该子元素又测量了它的子元素两次,依此类推,那么一次尝试布置整个界面就不得不做大量的工作,这将很难让应用保持良好的性能。

如果布局由于某种原因需要多次测量,Compose 会提供一个特殊的系统,即“固有特性测量”。如需详细了解此功能,请参阅 Compose 布局中的固有特性测量

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

修饰符

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

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

修饰符是标准的 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 = { /*...*/ }
    )
}