Compose 修饰符

修饰符允许您装饰或增强可组合项。修饰符允许您执行以下操作

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

修饰符是标准的 Kotlin 对象。通过调用 Modifier 类函数之一来创建修饰符

@Composable
private fun Greeting(name: String) {
    Column(modifier = Modifier.padding(24.dp)) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

Two lines of text on a colored background, with padding around the text.

您可以将这些函数链接在一起以组合它们

@Composable
private fun Greeting(name: String) {
    Column(
        modifier = Modifier
            .padding(24.dp)
            .fillMaxWidth()
    ) {
        Text(text = "Hello,")
        Text(text = name)
    }
}

The colored background behind the text now extends the full width of the device.

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

  • padding 在元素周围添加空格。
  • fillMaxWidth 使可组合项填充其父级提供的最大宽度。

最佳实践是让所有可组合项都接受 modifier 参数,并将该修饰符传递给发出 UI 的第一个子项。这样做可以使您的代码更具可重用性,并使其行为更具可预测性和直观性。有关更多信息,请参阅 Compose API 指南,元素接受并尊重 Modifier 参数

修饰符的顺序很重要

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

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

The entire area, including the padding around the edges, responds to clicks

在上面的代码中,整个区域都可点击,包括周围的填充,因为 padding 修饰符已 clickable 修饰符之后应用。如果修饰符顺序颠倒,则 padding 添加的空间不会对用户输入做出反应

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

The padding around the edge of the layout no longer responds to clicks

内置修饰符

Jetpack Compose 提供了一系列内置修饰符,可帮助您装饰或增强可组合项。以下是一些您将用于调整布局的常用修饰符。

paddingsize

默认情况下,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 { /*...*/ }
    }
}

Child image is bigger than the constraints coming from its parent

在此示例中,即使父级 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 { /*...*/ }
    }
}

The image height is as big as its parent

要在元素周围添加填充,请设置 padding 修饰符。

如果要向文本基线上方添加填充,以便从布局顶部到基线的距离达到特定距离,请使用 paddingFromBaseline 修饰符

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

Text with padding above it

偏移量

要相对于布局的原始位置定位布局,请添加 offset 修饰符并在xy 轴上设置偏移量。偏移量可以为正,也可以为非正。 paddingoffset 之间的区别在于,向可组合项添加 offset 不会更改其测量值

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

Text shifted to the right side of its parent container

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

offset 修饰符提供了两个重载 - offset 将偏移量作为参数,以及 offset 接收 lambda。有关何时使用每个偏移量以及如何优化性能的更深入信息,请阅读 Compose 性能 - 尽可能延迟读取 部分。

Compose 中的范围安全性

在 Compose 中,有些修饰符只能在应用于某些可组合项的子项时使用。Compose 通过自定义范围来强制执行此操作。

例如,如果要使子项与父级 Box 一样大,而不影响 Box 的大小,请使用 matchParentSize 修饰符。 matchParentSize 仅在 BoxScope 中可用。因此,它只能在 Box 父级内的子项上使用。

范围安全性可防止您添加在其他可组合项和范围内不起作用的修饰符,并节省了反复试验的时间。

作用域修饰符会通知父级有关父级应该了解的子级的一些信息。这些也通常称为父级数据修饰符。它们的内部与通用修饰符不同,但从使用角度来看,这些差异无关紧要。

matchParentSizeBox

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

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

在下面的示例中,子级 Spacer 从其父级 Box 获取其大小,而父级 Box 又从最大的子级(在本例中为 ArtistCard)获取其大小。

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

Gray background filling its container

如果使用 fillMaxSize 代替 matchParentSize,则 Spacer 将占用父级允许的所有可用空间,从而导致父级扩展并填充所有可用空间。

Gray background filling the screen

weightRowColumn

正如您在上一节关于 填充和大小 中所看到的,默认情况下,可组合项的大小由其包装的内容定义。您可以使用 weight 修饰符在其父级内设置可组合项的大小,该修饰符仅在 RowScopeColumnScope 中可用。

让我们以一个包含两个 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)
        ) {
            /*...*/
        }
    }
}

The image width is twice text width

提取和重用修饰符

可以将多个修饰符链接在一起以装饰或增强可组合项。此链通过 Modifier 接口创建,该接口表示单个 Modifier.Elements 的有序、不可变列表。

每个 Modifier.Element 代表一个单独的行为,例如布局、绘制和图形行为、所有手势相关、焦点和语义行为,以及设备输入事件。它们的顺序很重要:首先添加的修饰符元素将首先应用。

有时,通过将修饰符链实例提取到变量中并提升到更高的作用域,在多个可组合项中重用相同的修饰符链实例可能是有益的。它可以提高代码可读性或出于以下几个原因帮助提高应用程序的性能

  • 当对使用它们的组合项进行重新组合时,修饰符的重新分配不会重复
  • 修饰符链可能非常长且复杂,因此重用相同链实例可以减轻 Compose 运行时在比较它们时需要执行的工作负载
  • 这种提取促进了整个代码库的代码整洁性、一致性和可维护性

重用修饰符的最佳实践

创建您自己的 Modifier 链并将其提取出来,以便在多个可组合组件上重用它们。仅保存修饰符完全没问题,因为它们类似于数据对象

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

在观察频繁变化的状态时提取和重用修饰符

在观察可组合项内部频繁变化的状态时,例如动画状态或 scrollState,可能会执行大量的重新组合。在这种情况下,您的修饰符将在每次重新组合时以及可能在每一帧中分配

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // Creation and allocation of this modifier will happen on every frame of the animation!
        modifier = Modifier
            .padding(12.dp)
            .background(Color.Gray),
        animatedState = animatedState
    )
}

相反,您可以创建、提取和重用修饰符的同一实例,并将其传递给可组合项,如下所示

// Now, the allocation of the modifier happens here:
val reusableModifier = Modifier
    .padding(12.dp)
    .background(Color.Gray)

@Composable
fun LoadingWheelAnimation() {
    val animatedState = animateFloatAsState(/*...*/)

    LoadingWheel(
        // No allocation, as we're just reusing the same instance
        modifier = reusableModifier,
        animatedState = animatedState
    )
}

提取和重用无作用域的修饰符

修饰符可以是无作用域的,也可以作用域到特定的可组合项。对于无作用域的修饰符,您可以轻松地将其作为简单的变量提取到任何可组合项之外

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

@Composable
fun AuthorField() {
    HeaderText(
        // ...
        modifier = reusableModifier
    )
    SubtitleText(
        // ...
        modifier = reusableModifier
    )
}

这与 Lazy 布局结合使用时尤其有利。在大多数情况下,您希望所有(可能数量众多)的项都具有完全相同的修饰符

val reusableItemModifier = Modifier
    .padding(bottom = 12.dp)
    .size(216.dp)
    .clip(CircleShape)

@Composable
private fun AuthorList(authors: List<Author>) {
    LazyColumn {
        items(authors) {
            AsyncImage(
                // ...
                modifier = reusableItemModifier,
            )
        }
    }
}

提取和重用作用域修饰符

在处理作用域到某些可组合项的修饰符时,您可以将其提取到尽可能高的级别,并在适当的地方重用

Column(/*...*/) {
    val reusableItemModifier = Modifier
        .padding(bottom = 12.dp)
        // Align Modifier.Element requires a ColumnScope
        .align(Alignment.CenterHorizontally)
        .weight(1f)
    Text1(
        modifier = reusableItemModifier,
        // ...
    )
    Text2(
        modifier = reusableItemModifier
        // ...
    )
    // ...
}

您应该只将提取的作用域修饰符传递给相同作用域的直接子项。有关此问题的重要性的更多参考,请参阅部分 Compose 中的作用域安全性

Column(modifier = Modifier.fillMaxWidth()) {
    // Weight modifier is scoped to the Column composable
    val reusableItemModifier = Modifier.weight(1f)

    // Weight will be properly assigned here since this Text is a direct child of Column
    Text1(
        modifier = reusableItemModifier
        // ...
    )

    Box {
        Text2(
            // Weight won't do anything here since the Text composable is not a direct child of Column
            modifier = reusableItemModifier
            // ...
        )
    }
}

提取修饰符的进一步链接

您可以通过调用 .then() 函数进一步链接或附加提取的修饰符链

val reusableModifier = Modifier
    .fillMaxWidth()
    .background(Color.Red)
    .padding(12.dp)

// Append to your reusableModifier
reusableModifier.clickable { /*...*/ }

// Append your reusableModifier
otherModifier.then(reusableModifier)

请记住,修饰符的顺序很重要!

了解更多

我们提供了 修饰符的完整列表,包括其参数和作用域。

有关如何使用修饰符的更多实践,您还可以学习 Compose 代码实验室中的基本布局 或参考 Now in Android 代码库

有关自定义修饰符以及如何创建它们的更多信息,请查看有关 自定义布局 - 使用布局修饰符 的文档。