Compose 修饰符

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

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

修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier 类函数来创建修饰符:

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

彩色背景上有两行文字,文字周围有内边距。

您可以将以下函数连在一起以将其组合起来:

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

文字后面的彩色背景现已延伸覆盖设备屏幕的全宽。

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

  • padding 在元素周围留出空间。
  • fillMaxWidth 使可组合项填充其父项为它提供的最大宽度。

最佳实践是让所有可组合项都接受 modifier 参数,并将该修饰符传递给其发出界面的第一个子级。这样做可以提高代码的可重用性,使其行为更可预测且更直观。如需了解详情,请参阅 Compose API 指南:接受元素和遵循修饰符参数

修饰符顺序很重要

修饰符函数的顺序非常重要。由于每个函数都会对上一个函数返回的 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 提供了一个内置修饰符列表,可帮助您修饰或扩充可组合项。以下是一些用于调整布局的常见修饰符。

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 { /*...*/ }
    }
}

子图片的尺寸大于其父项的约束条件

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

图片高度与其父项相同

如需在元素周围添加内边距,请设置 padding 修饰符。

如需在文本基线上方添加内边距,以实现从布局顶部到基线保持特定距离,请使用 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 修饰符,该修饰符中的正偏移值一律会将元素向右移。

offset 修饰符提供了两种重载 - 将偏移量作为参数的 offset 以及采用 lambda 的 offset。如需深入了解何时使用这些修饰符以及如何针对性能进行优化,请仔细阅读 Compose 性能 - 尽可能延迟读取部分。

Compose 中的作用域安全

在 Compose 中,有一些修饰符只能应用于某些可组合项的子项。Compose 通过自定义作用域强制实施此安全机制。

例如,如果您希望使某个子项与父项 Box 同样大,而不影响 Box 尺寸,请使用 matchParentSize 修饰符。matchParentSize 仅适用于 BoxScope,因此只能对 Box 父级中的子级使用。

作用域安全机制可防止您添加在其他可组合项和作用域中不起作用的修饰符,还可节省试错的时间。

限定作用域的修饰符会将父项应知晓的关于子项的一些信息告知父项。这些修饰符通常称为“父项数据修饰符”。它们的内部构件与通用修饰符不同,但从使用角度来看,这些差异并不重要。

matchParentSize(在 Box 中)

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

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

在以下示例中,子项 Spacer 从其父项 Box 获取自己的尺寸,在这种情况下,后者又会从其最大的子项 ArtistCard 中获取自己的尺寸。

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

填充容器的灰色背景

如果使用 fillMaxSize 代替 matchParentSizeSpacer 将占用父项允许的所有可用空间,反过来使父项展开并填满所有可用空间。

填充屏幕的灰色背景

RowColumn 中的 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)
        ) {
            /*...*/
        }
    }
}

图片宽度是文字宽度的两倍

提取和重复使用修饰符

您可以将多个修饰符链接起来,以修饰或扩充可组合项。此链是通过 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
    )
}

与延迟布局结合使用时,这尤为有用。在大多数情况下,建议对所有潜在的重要项目使用完全相同的修饰符:

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 代码库

如需详细了解自定义修饰符以及如何创建这类修饰符,请参阅介绍自定义布局 - 使用布局修饰符的文档。