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 获取其大小,而父级又从最大的子级(本例中为 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)

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

了解更多

我们提供了 修饰符的完整列表,包括它们的 parameters 和 scope。

要更深入地了解如何使用修饰符,您还可以浏览 Compose 代码实验室中的基本布局 或参考 Now in Android 代码库

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