在经历了漫长的等待后,Compose 1.8 终于是姗姗来迟,让我们一起简单看看更新亮点吧
本文翻译自 Android Developers Blog: What’s new in the Jetpack Compose April ’25 release ,原作者:Jolanda Verhoef,译者对附带的部分链接和 API 参考也并入其中,方便连贯阅读。
今天,作为 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 Foundation和Compose UI库。否则,Autofill 功能将无法正常工作。
Autofill (自动填充)
简介
Autofill 是一项简化数据输入的服务。它能用户在填写表单、登录、结账等流程中无需手动输入每个细节。现在,您可以将此功能集成到您的 Compose 应用程序中。
在 Compose 文本字段中设置 Autofill 非常简单:
-
设置
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 元素 } } -
处理凭据保存(适用于新信息或更新后的信息):
- a. 通过导航隐式处理:如果用户导航离开当前页面,
commit()将被自动调用 - 无需编写额外代码! - b. 通过按钮显式处理:要在用户提交表单(例如,通过点击按钮)时触发凭据保存,请获取本地的
AutofillManager并调用commit()。
@Composable fun ExplicitCommitButton() { // 获取 AutofillManager (如果可用) val autofill = LocalAutofill.current val autofillTree = LocalAutofillTree.current // 用于构建 Autofill 节点树 Button(onClick = { autofill?.commit() // 显式触发 Autofill 保存 }) { Text("登录并保存") } } - a. 通过导航隐式处理:如果用户导航离开当前页面,
有关如何在您的应用程序中实现 Autofill 的完整详细信息,请 参阅 Autofill in Compose 文档 继续往下读
附:完整文档
某些应用(例如密码管理器)可以使用用户提供的数据来填充其他应用中的组件。这些填充其他应用组件的应用称为 Autofill 服务 (autofill services) 。Autofill 框架负责管理应用和 Autofill 服务之间的通信。
填写凭据和表单是一项耗时且容易出错的任务。Autofill 可以帮助用户节省填写字段的时间,并最大限度地减少用户输入错误。
只需几行代码,您就可以在 Compose 中实现 Autofill。此功能为用户提供了以下好处:
-
填充凭据
Autofill 允许用户通过以下方式填充其凭据:
- 当用户点击设置了 Autofill 语义的字段时,系统会向用户显示 Autofill 建议。
- 系统向用户显示 Autofill 建议,并根据用户的输入进行筛选。
-
保存凭据
用户可以通过 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 保存数据:
-
创建一个本地变量来持有 Autofill 管理器,可以通过以下方式获取:
val autofillManager = LocalAutofillManager.current -
在您的
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 } ) } -
根据需要,通过按钮点击提交 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 保存数据
根据您的凭据提供程序,当您使用 NewUsername 和 NewPassword 内容类型时,用户可能会在键盘中看到一个按钮,用于 *建议强密码 (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()
)
}
您可以自定义大小调整行为,通过设置最小和/或最大字体大小,并定义步长。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.StartEllipsis 或 TextOverflow.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),这有助于提高性能,尤其是在将其添加到 LazyColumn 或 LazyRow 中的项目上时。
Compose 1.9 将在此底层 API 的基础上添加更高级别的抽象,以简化常见的用例。
补充:onGloballyPositioned vs onLayoutRectChanged
onGloballyPositioned:每次 Composable 在窗口上的位置或大小发生 任何 变化(即使是很小的像素移动)时都会触发回调。这在需要精确位置信息的场景下很有用,但在频繁变化的场景(如滚动列表)中可能导致性能问题,因为它会触发大量回调执行。onLayoutRectChanged:它也提供了 Composable 的布局矩形信息,但设计上更侧重于性能。它内部可以配置防抖和节流逻辑(通过debounceMillis和throttleMillis参数),这意味着回调不会在每次微小变化时都触发,而是会在变化稳定后或以一定频率触发,从而显著减少回调次数和性能开销。这使得它非常适合用于判断 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。
将 throttleMillis 和 debounceMillis 都指定为 0 将导致每次位置更改时都执行回调。为两者指定非零值将导致同时满足两个条件。指定非零的 throttleMillis 但 debounceMillis 为零,等效于为 throttleMillis 和 debounceMillis 提供相同的值。
| 参数 (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))
}
}
}
}
补充:LookaheadScope 和 animateBounds
LookaheadScope提供了一个“预计算”布局阶段。在这个阶段,Compose 可以知道 Composable 在 下一次 布局传递中的目标大小和位置。animateBounds利用LookaheadScope提供的预计算信息,在当前布局状态和目标布局状态之间自动创建平滑的动画过渡。这极大地简化了因状态变化导致的大小和位置变化的动画实现。
提升 API 稳定性 (Increased API stability)
Jetpack Compose 使用 @Experimental 注解来标记那些在不同版本之间可能发生变化的 API,适用于那些需要比库的 alpha 阶段更长时间才能稳定的功能。我们听到了您的反馈,许多功能被标记为实验性已经有一段时间了,但却没有发生变化,这造成了一种不稳定的感觉。我们正在积极审视并稳定现有的实验性 API——在 UI 和 Foundation 模块中,我们已将实验性 API 的数量从 1.7 版本中的 172 个减少到 1.8 版本中的 70 个。我们计划在未来的版本中继续在各个模块中推进这种稳定化趋势。
废弃 ContextualFlowRow 和 ContextualFlowColumn
作为减少实验性注解工作的一部分,我们识别出在最近版本中添加的一些 API,它们对于其用例来说并非最佳解决方案。这导致我们决定废弃在 Foundation 1.7 中添加的实验性 ContextualFlowRow 和 ContextualFlowColumn API。如果您需要这些被废弃的功能,我们目前的建议是复制代码实现并根据需要进行调整,同时我们正在制定计划,以便未来的组件能够更好地涵盖这些功能。
译者:我靠,正准备用呢,你给我废弃了……
相关的 API FlowRow 和 FlowColumn 现在是稳定的;然而,在上一版本中添加的新的 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!