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 修饰符,并在 x 轴和 y 轴上设置偏移。偏移可以是正值也可以是非正值。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,第二个 Box 的宽度为 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 基础布局 Codelab 或参考 Now in Android 代码库

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