Jetpack Compose 1.8.0 上新:自动填充密码、自适应大小文本、动画边界……

2,945 阅读16分钟

在经历了漫长的等待后,Compose 1.8 终于是姗姗来迟,让我们一起简单看看更新亮点吧

本文翻译自 Android Developers Blog: What’s new in the Jetpack Compose April ’25 release ,原作者:Jolanda Verhoef,译者对附带的部分链接和 API 参考也并入其中,方便连贯阅读。

Jetpack Compose Logo and Feature Highlights

今天,作为 Compose April ‘25 Bill of Materials 的一部分,我们发布了 Jetpack Compose 的 1.8 版本。Jetpack Compose 是 Android 现代化的原生 UI 工具包,已被 众多开发者 采用。此版本包含了 Autofill (自动填充)、多项文本改进、可见性跟踪以及动画化 Composable 大小和位置的新方法等新功能。它还稳定了许多实验性 API 并修复了大量 bug。

要使用此版本,请将您的 Compose BOM 版本升级到 2025.04.01

dependencies {
    // ... 其他依赖
    implementation(platform("androidx.compose:compose-bom:2025.04.01"))
}

注意: 如果您未使用 Bill of Materials (BOM),请确保同时升级 Compose FoundationCompose UI 库。否则,Autofill 功能将无法正常工作。

Autofill (自动填充)

简介

Autofill 是一项简化数据输入的服务。它能用户在填写表单、登录、结账等流程中无需手动输入每个细节。现在,您可以将此功能集成到您的 Compose 应用程序中。

在 Compose 文本字段中设置 Autofill 非常简单:

  1. 设置 contentType 语义 (Semantics) :使用 Modifier.semantics 并为您的文本字段设置适当的 contentType。例如:

    // 简单的 TextField 状态管理示例
    @Composable
    fun SimpleTextField(
        modifier: Modifier = Modifier,
        contentType: ContentType,
        label: String = "" // 可选的标签
    ) {
        var text by remember { mutableStateOf("") }
    ​
        BasicTextField(
            value = text,
            onValueChange = { text = it },
            modifier = modifier
                .semantics {
                    this.contentType = contentType // 设置 Autofill 类型
                }
            // 可以添加 decorationBox 来实现类似 Material 的外观
            // decorationBox = { innerTextField -> ... }
        )
    }
    ​
    // 使用示例
    @Composable
    fun LoginForm() {
        Column {
            SimpleTextField(contentType = ContentType.Username, label = "用户名")
            SimpleTextField(contentType = ContentType.Password, label = "密码")
            // ... 其他 UI 元素
        }
    }
    
  2. 处理凭据保存(适用于新信息或更新后的信息):

    • a. 通过导航隐式处理:如果用户导航离开当前页面,commit() 将被自动调用 - 无需编写额外代码!
    • b. 通过按钮显式处理:要在用户提交表单(例如,通过点击按钮)时触发凭据保存,请获取本地的 AutofillManager 并调用 commit()
    @Composable
    fun ExplicitCommitButton() {
        // 获取 AutofillManager (如果可用)
        val autofill = LocalAutofill.current
        val autofillTree = LocalAutofillTree.current // 用于构建 Autofill 节点树
    ​
        Button(onClick = {
            autofill?.commit() // 显式触发 Autofill 保存
        }) {
            Text("登录并保存")
        }
    }
    

有关如何在您的应用程序中实现 Autofill 的完整详细信息,请 参阅 Autofill in Compose 文档 继续往下读

附:完整文档

某些应用(例如密码管理器)可以使用用户提供的数据来填充其他应用中的组件。这些填充其他应用组件的应用称为 Autofill 服务 (autofill services) 。Autofill 框架负责管理应用和 Autofill 服务之间的通信。

填写凭据和表单是一项耗时且容易出错的任务。Autofill 可以帮助用户节省填写字段的时间,并最大限度地减少用户输入错误。

只需几行代码,您就可以在 Compose 中实现 Autofill。此功能为用户提供了以下好处:

  1. 填充凭据

    Autofill 允许用户通过以下方式填充其凭据:

    • 当用户点击设置了 Autofill 语义的字段时,系统会向用户显示 Autofill 建议。
    • 系统向用户显示 Autofill 建议,并根据用户的输入进行筛选。
  2. 保存凭据

用户可以通过 Autofill 以下列方式保存凭据:

  • 当用户在启用了 Autofill 的字段中输入新的或更新的信息时,系统会触发一个保存对话框,提示用户保存信息。保存可以通过两种方式完成:

    • 显式地,通过提交信息(例如,通过按钮点击)
    • 隐式地,当用户导航离开页面时
  • 根据您的凭据提供程序 (credential provider):比如当字段设置了 ContentType.NewPassword 时,系统可能会向用户建议强密码。

您可以在应用中使用 Autofill 来简化用户检索已保存数据的过程。Autofill 通过 BasicTextField 以及所有基于该组件构建的 Material TextField

设置 Autofill

在您的设备或模拟器上使用 Autofill API 之前,您必须在“设置”中激活 Autofill。在那里,您可以指定一个凭据提供程序 (credential provider) 以便 Autofill 存储您的凭据。

显示如何指定凭据提供程序的设置页面

图 1. 显示如何指定凭据提供程序的设置页面。

使用 ContentType 将 Autofill 添加到文本字段

要指示 TextField 已启用 Autofill,请使用该字段可以接受的类型来设置 ContentType 语义。这会向 Autofill 服务指示哪种类型的用户数据可能与此特定字段相关。使用 ContentType.Username 来设置一个用户可以用其用户名填充的 TextField

@Composable
fun AutofillUsernameField() {
    var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
​
    TextField(
        value = textFieldValue,
        onValueChange = { textFieldValue = it },
        modifier = Modifier.semantics { contentType = ContentType.Username }
    )
}

通过设置 ContentType 语义,您的用户可以访问已保存在其设备凭据提供程序中的 Autofill 信息。例如,如果用户之前已通过其笔记本电脑上的 Chrome 浏览器登录您的应用,并通过凭据提供程序保存了密码,那么他们的凭据将通过 Autofill 提供给他们。

添加具有多种类型的 Autofill 字段

在某些情况下,您可能希望您的 TextField 接受不止一种 ContentType。例如,登录字段可能接受电子邮件地址或用户名。您可以使用 + 运算符将多个内容类型添加到您的 TextField 中。


@Composable
fun AutofillMultiTypeField() {
    var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
​
    TextField(
        value = textFieldValue,
        onValueChange = { textFieldValue = it },
        modifier = Modifier.semantics {
            contentType = ContentType.Username + ContentType.EmailAddress // 合并多种类型
        }
    )
}

有关可用于 Autofill 保存的所有数据类型,请参阅 ContentType 参考文档

使用 Autofill 填充数据

当您在 TextField 中添加 ContentType 后,无需执行任何其他操作,用户即可填充凭据。

当用户点击启用了 Autofill 的字段时,如果存储了相关数据,他们会在键盘上方的工具栏中看到一个提示条 (chip),提示他们填充凭据。

文本工具栏中显示已保存凭据的提示条

图 2. 文本工具栏中显示已保存凭据的提示条。

通过导航使用 Autofill 保存数据

Compose 会自动尝试确定用户何时从页面导航离开,并提交输入的凭据。一旦字段启用了 Autofill,当用户导航离开页面时,它将自动保存凭据信息,无需任何额外代码。

使用 Autofill 显式保存数据

要通过带有 Autofill 的文本字段显式保存新凭据,Autofill 上下文应由 Autofill 管理器提交 (commit)(或取消 (cancel))。LocalAutofillManager 会在必要时与 Autofill 框架进行通信。如果您想移除用户已输入的凭据,请调用 AutofillManager.cancel 来删除任何待处理的数据而不保存它们。

以下代码片段展示了如何使用按钮显式地通过 Autofill 保存数据:

  1. 创建一个本地变量来持有 Autofill 管理器,可以通过以下方式获取:

    val autofillManager = LocalAutofillManager.current
    
  2. 在您的 TextField(s) 中,通过 Modifier.semantics 添加所需的内容类型:

    val autofillManager = LocalAutofillManager.current
    ​
    Column {
        TextField(
            value = textFieldValue.value,
            onValueChange = { textFieldValue.value = it },
            modifier = Modifier.semantics { contentType = ContentType.NewUsername }
        )
    ​
        Spacer(modifier = Modifier.height(16.dp))
    ​
        TextField(
            value = textFieldValue.value,
            onValueChange = { textFieldValue.value = it },
            modifier = Modifier.semantics { contentType = ContentType.NewPassword }
        )
    }
    
  3. 根据需要,通过按钮点击提交 Autofill 上下文:

    val autofillManager = LocalAutofillManager.current
    ​
    Column {
        TextField(
            value = textFieldValue.value,
            onValueChange = { textFieldValue.value = it },
            modifier = Modifier.semantics { contentType = ContentType.NewUsername },
        )
    ​
        Spacer(modifier = Modifier.height(16.dp))
    ​
        TextField(
            value = textFieldValue.value,
            onValueChange = { textFieldValue.value = it },
            modifier = Modifier.semantics { contentType = ContentType.NewPassword },
        )
    ​
        // Submit button
        Button(onClick = { autofillManager?.commit() }) { Text("Reset credentials") }
    }
    

当用户导航离开屏幕时,commit会被调用。如果 提交 (Submit) 按钮与导航相关联,则无需调用 commit。如果您仍希望点击 提交 (Submit) 按钮触发保存对话框,请在此处添加 commit

当用户点击按钮时,他们将看到此 bottom sheet,提示他们将凭据保存到选定的凭据提供程序:

提示用户保存密码的底部工作表 图 3. 提示用户保存密码的底部工作表。

通过“建议强密码”使用 Autofill 保存数据

根据您的凭据提供程序,当您使用 NewUsernameNewPassword 内容类型时,用户可能会在键盘中看到一个按钮,用于 *建议强密码 (Suggest strong password) *。当他们点击此按钮时,会出现一个 bottom sheet,允许他们保存凭据。您无需实现任何其他内容即可让用户获得此体验。

键盘工具栏中的“建议强密码”提示条

图 4. 键盘工具栏中的“建议强密码”提示条。

提示用户使用强密码的底部工作表

图 5. 提示用户使用强密码的 bottom sheet。

问题排查

在调用“保存”用户流程时,如果您多次点击“以后再说 (Not now) ”,您的凭据提供程序可能不再显示此底部提示。要重新启用它并使其再次出现,您需要将此应用移出 阻止“记住此密码?(Remember this password?) ” 的列表(通常在 Autofill 服务的设置中)。

提示用户保存密码的底部工作表

图 6. 提示用户保存密码的底部工作表。

进一步自定义 Autofill

在典型的 Autofill 用户流程中,当启用了 Autofill 的组件被填充凭据后,它会改变颜色并高亮显示,以向用户表明 Autofill 已成功完成。

要自定义此高亮颜色,请使用 CompositionLocalProvider 并提供您想要的任何颜色给 LocalAutofillHighlightColor

val customHighlightColor = Color.Red // 定义自定义高亮颜色
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
​
// 使用 CompositionLocalProvider 提供自定义颜色
CompositionLocalProvider(LocalAutofillHighlightColor provides customHighlightColor) {
    TextField(
        value = textFieldValue,
        onValueChange = { textFieldValue = it },
        modifier = Modifier.semantics { contentType = ContentType.Username }
    )
}
​

默认的 Autofill 高亮颜色定义为 Color(0x4dffeb3b)

Text 相关

自适应大小

现在,当您将文本放置在容器内时,可以使用 BasicText 中的 autoSize 参数,让文本大小自动适应容器大小:

Box {
    BasicText(
        text = "Hello World",
        maxLines = 1,
        autoSize = TextAutoSize.StepBased()
    )
}

动态调整大小的 "Hello World" 文本

您可以自定义大小调整行为,通过设置最小和/或最大字体大小,并定义步长。Compose Foundation 1.8 包含了这个新的 BasicText 重载,Material 1.4 也将很快跟进,提供更新的 Text 重载。

fun StepBased(
    minFontSize: TextUnit = TextAutoSizeDefaults.MinFontSize,
    maxFontSize: TextUnit = TextAutoSizeDefaults.MaxFontSize,
    stepSize: TextUnit = 0.25.sp
): TextAutoSize

更多文本溢出选项

此外,Compose 1.8 通过新的 TextOverflow.StartEllipsisTextOverflow.MiddleEllipsis 选项增强了文本溢出处理,允许您在文本行的开头或中间显示省略号。

@Composable
fun TextOverflowExample() {
    val text = "This is a long text that will overflow"
    Column(Modifier.width(200.dp)) {
      // 默认尾部省略
      Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis)
      // 头部省略
      Text(text, maxLines = 1, overflow = TextOverflow.StartEllipsis)
      // 中间省略
      Text(text, maxLines = 1, overflow = TextOverflow.MiddleEllipsis)
    }
}

文本溢出处理,在行首和行中显示省略号

扩展的对 HTML 格式的支持

最后,我们扩展了 AnnotatedString 对 HTML 格式的支持,新增了对项目符号列表 (<ul>, <li>) 的支持:

@Composable
fun HtmlListExample() {
    Text(
        AnnotatedString.fromHtml(
            """
            <h1>HTML content</h1>
            <ul>
              <li>Hello,</li>
              <li>World</li>
            </ul>
            """.trimIndent()
        )
    )
}

包含两个项目的符号列表

可见性追踪 (Visibility tracking)

Compose UI 1.8 引入了一个新的 Modifier:onLayoutRectChanged。这个 API 能解决现有 onGloballyPositioned Modifier 的许多用例——但开销却要小得多。onLayoutRectChanged Modifier 可以根据需要对回调进行防抖 (debounce) 和节流 (throttle),这有助于提高性能,尤其是在将其添加到 LazyColumnLazyRow 中的项目上时。

Compose 1.9 将在此底层 API 的基础上添加更高级别的抽象,以简化常见的用例。

补充:onGloballyPositioned vs onLayoutRectChanged

  • onGloballyPositioned:每次 Composable 在窗口上的位置或大小发生 任何 变化(即使是很小的像素移动)时都会触发回调。这在需要精确位置信息的场景下很有用,但在频繁变化的场景(如滚动列表)中可能导致性能问题,因为它会触发大量回调执行。
  • onLayoutRectChanged:它也提供了 Composable 的布局矩形信息,但设计上更侧重于性能。它内部可以配置防抖和节流逻辑(通过 debounceMillisthrottleMillis 参数),这意味着回调不会在每次微小变化时都触发,而是会在变化稳定后或以一定频率触发,从而显著减少回调次数和性能开销。这使得它非常适合用于判断 Composable 是否在屏幕上可见、可见比例等场景。

这个新 API 为依赖于 Composable 在屏幕上可见性的功能提供了基础。例如,可以用来实现:

  • 曝光跟踪 (Impression Tracking): 当列表项滚动到可见区域时记录一次曝光。
  • 按需加载或动画: 当某个元素进入视口时才开始加载数据或播放动画。
  • 视频自动播放/暂停: 当视频组件滚动进出可见区域时自动控制播放状态。

onGloballyPositioned

fun Modifier.onGloballyPositioned(
    onGloballyPositioned: (LayoutCoordinates) -> Unit
): Modifier

当内容的全局位置可能已更改时,使用该元素的 LayoutCoordinates 调用 onGloballyPositioned。请注意,它将在 Composition 之后、坐标最终确定时被调用。

LayoutCoordinates 可用时,此回调将至少被调用一次,并且每当元素在窗口内的位置发生变化时都会被调用。但是,不保证每次修改元素的相对于屏幕 的位置发生变化时都会调用它。例如,系统可能会在窗口内移动内容而不会触发回调。如果您使用 LayoutCoordinates 来计算屏幕上的位置,而不仅仅是窗口内的位置,您可能不会收到回调。

用法示例:

Column(
    Modifier.onGloballyPositioned { coordinates ->
        // 这将是 Column 的大小。
        coordinates.size
        // Column 相对于应用程序窗口的位置。
        coordinates.positionInWindow()
        // Column 相对于 Compose Root 的位置。
        coordinates.positionInRoot()
        // 这些将是提供给布局的对齐线 (alignment lines)(此处对于 Column 是空的)。
        coordinates.providedAlignmentLines
        // 这将是对应于 Column 父级的 LayoutCoordinates 实例。
        coordinates.parentLayoutCoordinates
    }
) {
    Box(Modifier.size(20.dp).background(Color.Green))
    Box(Modifier.size(20.dp).background(Color.Blue))
}

onLayoutRectChanged

fun Modifier.onLayoutRectChanged(
    throttleMillis: Long = 0,
    debounceMillis: Long = 64,
    callback: (RelativeLayoutBounds) -> Unit
): Modifier

使用此布局节点相对于 Composition 根坐标系的位置、以及屏幕坐标和窗口坐标来调用 callback。这将在布局传递 (layout pass) 之后被调用。此 API 允许使用节流 (throttling) 和防抖 (debouncing) 参数,以在变化率较高(例如滚动)期间调节 callback 被调用的频率。

指定 throttleMillis 将阻止 callback 在该时间段内执行超过一次。指定 debounceMillis 将延迟 callback 的执行,直到该时间量内没有新的位置变化发生,并在该时间到期时安排执行 callback

throttleMillisdebounceMillis 都指定为 0 将导致每次位置更改时都执行回调。为两者指定非零值将导致同时满足两个条件。指定非零的 throttleMillisdebounceMillis 为零,等效于为 throttleMillisdebounceMillis 提供相同的值。

参数 (Parameters)描述
throttleMillis: Long = 0时间段(以毫秒为单位),用于阻止 callback 在该时间段内执行超过一次。
debounceMillis: Long = 64时间段(以毫秒为单位),用于延迟 callback 的执行,直到该时间量内没有新的位置变化发生。
callback: (RelativeLayoutBounds) -> Unit回调函数,接收包含相对边界信息的 RelativeLayoutBounds 对象。

动画化 Composable 边界 (Animate composable bounds)

去年我们引入了 shared element transitions(共享元素过渡),用于在您的应用中平滑地动画化内容。

关于共享元素动画,可参考站内的这篇 最清晰的 Compose 共享元素过渡入门文章

1.8 Animation 模块将 LookaheadScope 提升为稳定版,包含了许多性能和稳定性改进,并并引入了一个新的 Modifier:animateBounds。当在 LookaheadScope 内部使用时,此 Modifier 会在该 Composable 的大小和屏幕位置发生变化时自动对其进行动画处理:

@Composable
fun AnimateBoundsExample() {
    var expanded by remember { mutableStateOf(false) }
​
    // LookaheadScope 是 animateBounds 的必要外部作用域
    LookaheadScope {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .clickable { expanded = !expanded } // 点击切换状态
        ) {
            Box(
                Modifier
                    .align(Alignment.Center) // 居中对齐
                    // 根据 expanded 状态改变宽度和偏移量
                    .width(if (expanded) 180.dp else 110.dp)
                    .offset(x = if (expanded) 0.dp else 100.dp)
                    // !!! 核心:使用 animateBounds Modifier !!!
                    .animateBounds(
                        lookaheadScope = this@LookaheadScope, // 传入 LookaheadScope 实例
                    )
                    .background(Color.LightGray, shape = RoundedCornerShape(12.dp))
                    .height(50.dp)
            ) {
                Text("Layout Content", Modifier.align(Alignment.Center))
            }
        }
    }
}

描绘 Composable 边界动画的动图

补充:LookaheadScopeanimateBounds

  • LookaheadScope 提供了一个“预计算”布局阶段。在这个阶段,Compose 可以知道 Composable 在 下一次 布局传递中的目标大小和位置。
  • animateBounds 利用 LookaheadScope 提供的预计算信息,在当前布局状态和目标布局状态之间自动创建平滑的动画过渡。这极大地简化了因状态变化导致的大小和位置变化的动画实现。

提升 API 稳定性 (Increased API stability)

Jetpack Compose 使用 @Experimental 注解来标记那些在不同版本之间可能发生变化的 API,适用于那些需要比库的 alpha 阶段更长时间才能稳定的功能。我们听到了您的反馈,许多功能被标记为实验性已经有一段时间了,但却没有发生变化,这造成了一种不稳定的感觉。我们正在积极审视并稳定现有的实验性 API——在 UI 和 Foundation 模块中,我们已将实验性 API 的数量从 1.7 版本中的 172 个减少到 1.8 版本中的 70 个。我们计划在未来的版本中继续在各个模块中推进这种稳定化趋势。

废弃 ContextualFlowRowContextualFlowColumn

作为减少实验性注解工作的一部分,我们识别出在最近版本中添加的一些 API,它们对于其用例来说并非最佳解决方案。这导致我们决定废弃在 Foundation 1.7 中添加的实验性 ContextualFlowRowContextualFlowColumn API。如果您需要这些被废弃的功能,我们目前的建议是复制代码实现并根据需要进行调整,同时我们正在制定计划,以便未来的组件能够更好地涵盖这些功能。

译者:我靠,正准备用呢,你给我废弃了……

相关的 API FlowRowFlowColumn 现在是稳定的;然而,在上一版本中添加的新的 overflow 参数现在已被废弃

补充:FlowRow/Column vs ContextualFlowRow/Column

  • FlowRow / FlowColumn:提供基本的流式布局,当一行/一列放不下所有子项时,会自动换行/换列。它们现在是稳定的 API,适用于常见的标签组、选项组等场景。
  • ContextualFlowRow / ContextualFlowColumn (已废弃):旨在提供更复杂的溢出处理逻辑,例如在溢出时显示“+N”按钮。由于实现和 API 设计被认为不够理想,已被废弃。

核心功能的改进和修复

根据开发者的反馈,我们在核心库中发布了一些特别需要的功能和 bug 修复:

  • 测试中的无障碍检查 (Accessibility checks in tests) :在您的 Espresso 测试中使用 enableAccessibilityChecks 来自动测试应用中常见的无障碍问题 (例如,缺少内容描述、可点击区域过小等)。
  • 使对话框实现 Edge-to-edge (Make dialogs go edge to edge) :当全屏显示时,对话框现在会考虑屏幕的完整尺寸,并将在系统栏 (状态栏、导航栏) 后面绘制内容,提供更沉浸式的体验。
  • 更轻松地测试 ClickableText (Easier testing of ClickableText) :通过新的测试断言 performFirstLinkClick,验证用户点击链接时的正确行为。
  • 允许自定义过度滚动效果 (Allow customizing overscroll) :所有列表 (如 LazyColumn, LazyRow) 现在都有新的重载,允许您传入一个自定义的 OverscrollEffect。这意味着您可以改变或禁用列表滚动到边缘时的拉伸或发光效果。

即刻开始!

我们非常感谢大家向我们的 issue tracker 提交的所有 bug 报告和功能请求——它们帮助我们改进 Compose 并构建您需要的 API。请继续提供您的反馈,帮助我们让 Compose 变得更好。

Happy composing!