这次的版本没有什么特别大的架构变动,基本属于那种 “Compose 开始补齐 UI Framework 能力” 的版本,比如:图形管线、富文本编辑、程序化文本选择、手势驱动动画、Grid、Credential Manager、启动性能等等。
Mesh Gradient 和完整 WCG/HDR 管线
以前 Compose 自带的 Gradient 基本还是 Linear、Radial、Sweep 这类常见的“一条轴或者一个中心点”的渐变,而新增的 Mesh Gradient 现在可以定义一个二维网格,在不同顶点指定位置和颜色,然后 GPU 在这些控制点之间进行插值,也就是可以做出现在设计稿里很常见的那种多色、有机、流体感背景。
对应的 API 大致为:
val gradientPainter = remember {
MeshGradientPainter(1, 1) {
setVertex(0, 0, Offset(0f, 0f), Color.Red)
setVertex(0, 1, Offset(1f, 0f), Color.Blue)
setVertex(1, 0, Offset(0f, 1f), Color.Green)
setVertex(1, 1, Offset(1f, 1f), Color.Yellow)
}
}
Box(
Modifier.paint(gradientPainter)
)
这里比较有意思的是它在 alpha 阶段最初叫
Modifier.meshGradient(),最后稳定下来的时候, Google 把 API 改成了MeshGradientPainter + Modifier.paint()。
这部分 API 底层用硬件加速的 Canvas.drawMesh,也就是 Google 最后还是把 Mesh Gradient 定位成一种 Painter/绘制能力,没有把它当成 Modifier 自己的一套特殊渲染机制。
实际上这对于以后动画化 Mesh Gradient 也更合理:你可以修改网格顶点、颜色,然后正常进入 Compose 绘制体系。
更重要的其实是 Wide Color Gamut + HDR ,Compose 以前虽然 Color 本身可以支持 Display P3、Adobe RGB 等 ColorSpace,但进入 Paint、Shader、Android Canvas 的过程,部分路径会被转换/压缩回 sRGB,现在 1.12 把这条链真正补通:
Compose Color → Paint / Shader → Canvas → Android platform rendering
也就是 API 29+ 可以保留 Display P3、Adobe RGB 等非 sRGB 色彩,不会再在 Compose 中间环节被 clamp,平台不支持的 ColorSpace、系统版本过低这些情况,才会安全回退到 sRGB。
BasicTextField
虽然之前 Compose 显示 AnnotatedString,也能通过 OutputTransformation 给 TextField 显示结果加样式,但那种方案其实类似于数据还是普通文本,只是在绘制输出的时候给某些字符穿一件不同的衣服。
比如电话号码 12345678900 状态里还是这一串字符,屏幕上可以显示成 123 4567 8900 ,也可以显示时加上一些颜色,然后 Compose 1.12 新增了另一层能力:
state.edit {
addStyle(
SpanStyle(
fontWeight = FontWeight.Bold,
color = Color.Blue
),
start = 0,
end = 9
)
}
也就是现在 TextFieldBuffer 本身可以 addStyle()、getSpanStyles()、getParagraphStyles(),同时 TextFieldState 也有 textStyles ,样式可以跟着编辑状态一起存在,可以查询、修改、删除,并且能够跨 configuration change 保存。
也就是 Compose 自己的 TextField 架构更接近一套 Markdown Editor、富文本笔记、之类的见即所得编辑器,比如以前你写这些东西经常会陷入:
原始字符串
↓
AnnotatedString
↓
selection offset 映射
↓
IME offset
↓
Transformation offset
↓
Undo/Redo
然后稍微改一下文本,所有 span range 都得自己维护,现在 Style 本身进入 TextField Editing Model 里面, 等于是状态模型层面的增强,整套可用性会提高很多:
val state = rememberTextFieldState("Formatted text in Compose 1.12")
// Apply bold and color styles to a range of text
state.edit {
addStyle(
SpanStyle(fontWeight = FontWeight.Bold, color = Color.Blue),
start = 0,
end = 9
)
}
// Query active styles from TextFieldState
val currentStyles = state.textStyles
SelectionState
从 1.12 开始,SelectionState 支持把“选中文字”变成可以控制的 State ,比如:
val selectionState = rememberSelectionState()
SelectionContainer(
state = selectionState
) {
Text(...)
}
之后程序可以直接通过这些 API 进行操作:
selectionState.selectAll()
selectionState.clear()
selectionState.select(...)
selectionState.extendSelectionByWord()
还可以观察:
selectionState.selectedTexts
甚至通过 getSelectableTexts() 获得 SelectionContainer 内所有可以选择的文本,再按照全局 range 跨多个 Composable 选中内容。
也就是以前 SelectionContainer 基本是:“我允许用户长按,然后 Compose 帮我显示 selection handles。”
现在 selection 开始成为可以操作的数据,例如一个 AI Chat:
回答第一段
回答第二段
代码块
回答第三段
程序可以直接实现:
“全选回答”、“选中某一段”、“从当前位置扩展一个单词”、“获取当前选中的所有 AnnotatedString”。
@Composable
fun ProgrammaticSelectionExample() {
val selectionState = rememberSelectionState()
Column {
Button(
onClick = { selectionState.selectAll() },
modifier = Modifier.disableSelectionClearOnTap()
) {
Text("Select All")
}
SelectionContainer(state = selectionState) {
Text("Text content to be selected programmatically.")
}
}
}
另外 1.12 还支持拖动 selection 超过 viewport 时自动滚动,这类细节对长文阅读器、编辑器就很实用。
Deferred Animation
这个感觉是这次最有意思的一个更新,DeferredTargetAnimation 正式脱离 Experimental,同时增加:
DeferredAnimatedContent
DeferredAnimatedVisibility
核心场景就是 Predictive Back,假设用户从屏幕左边往右滑,动画前半段可以通过手指控制:
手指 20%
页面 scale / offset 20%
手指 50%
页面 scale / offset 50%
但当用户松手以后,动画就需要:
当前手势位置 + 当前速度
↓
Compose Transition 接管
↓
继续动画到最终状态
所以 Compose 1.12 的 Deferred Transition 就是专门把这两阶段连接起来:
Gesture controlled phase
↓
defer()
↓
手动修改 scale / offset
↓
用户松手
↓
自动 Transition 接管
↓
速度继续传递
↓
最终 Target State
这里 Google 甚至特别强调了 velocity transfer:自动动画接手时会把之前用户手势的速度继承过去,不然就很容易出现一种 UI 感:
用户快速一甩
↓
松手
↓
动画突然像重新启动一样慢下来
也就是现在动画能够延续用户松手瞬间的运动趋势,然后 permitTransformDuringDeferredTransition 又可以进一步处理 Shared Element,让 shared element 在 deferred 阶段跟随 parent 一起变换,所以这一整套其实是在建立统一动画基础设施,比如:
Predictive Back、drag-to-dismiss、interactive navigation、gesture-driven transition
Grid Named Areas
类似把 CSS Grid 的思路搬进 Compose,1.12 的 Experimental Grid 新增 Named Areas,比如:
@OptIn(ExperimentalGridApi::class)
@Composable
fun DashboardLayout() {
Grid(
config = {
area("header", row = 0, column = 0, rowSpan = 1, columnSpan = 2)
area("sidebar", row = 1, column = 0)
area("content", row = 1, column = 1)
gap(16.dp)
}
) {
HeaderSection(modifier = Modifier.gridItem(areaId = "header"))
NavigationSidebar(modifier = Modifier.gridItem(areaId = "sidebar"))
MainContentView(modifier = Modifier.gridItem(areaId = "content"))
}
}
甚至还有更骚的,不过不是这部分内容而已:
以前这种 Dashboard,一般要给每个元素写 row = 1 、column = 0 、span = 2
+----------------------+
| Header |
+--------+-------------+
| Sidebar| Content |
| | |
+--------+-------------+
现在可以先声明:
area("header", ...)
area("sidebar", ...)
area("content", ...)
组件只需要 Modifier.gridItem(areaId = "header"),做到 Layout Structure 与 Component 分离 ,比如以后 Compact:
header
content
sidebar
Expanded:
header header
sidebar content
你只需要改变 Grid Configuration,里面的组件依旧只知道:
我是 sidebar
我是 content
这明显是给 Android 现在大力推的 Phone + Foldable + Tablet + Desktop Window + XR 实现自适应布局服务的。
性能
首先是一个新支持优化,以前如果想表达 userId 或 screenName 改变时执行一次副作用,一般需要写:
LaunchedEffect(userId, screenName) { ... }
但如果里面根本没有 coroutine 工作,这实际上为了一个很轻的 side effect 做了比较重的协程相关操作,然后1.12 给 SideEffect 增加 key:
SideEffect(userId, screenName) {
analytics.log(...)
}
Google 给出的 microbenchmark 是:
| API | 新 Keyed SideEffect |
|---|---|
对比 LaunchedEffect | 最多快约 90% |
对比 DisposableEffect | 快 20% |
也就是在特定 Effect 调度场景里的新 API 可以做到更好的性能而且执行顺序也不同:
SideEffect会比DisposableEffect和LaunchedEffect更早执行,所以不能看见 90% 就机械替换,,Google 也专门提醒了这一点。
另外还有就是 Startup Performance,Google 提到了:
Compose 1.12 的 TTID 已经在他们的 benchmark 中做到与 Views comparable。
也就是冷启动到第一帧出现 的时间,终于基本追上传统 View,而在今年 5 月 Google 公布的 Pokedex Hero Benchmark 里,Compose 1.11 里,TTID 比 Views 慢 2.5%,TTFD 比 Views 慢 13.0%,所以这个提升还是很明显的。
另外滚动性能从 Compose 1.9 开始已经达到同样的 0.21% jank rate。
其他
现在 Compose TextField 现在可以通过新的 credentialRequest semantics 接 Android Credential Manager,在 API 34+ 直接通过 Autofill Framework 集成,低版本继续通过 androidx.credentials 处理,所以 Passkey、保存的账号密码、Sign-in credential 能更自然地进入 Compose TextField 输入流程。
而 KeyboardType 也增加了:
Date
Time
DateTime
SignedDecimal
另外 downloadable font 也开始支持 variable font variation settings。
然后 Compose 组件还增加了自动 click / focus navigation sound,同时提供 SoundEffectOnInteraction 关闭。
Graphics 还有个挺实用的新 API:LayerOutsets ,可以扩大 GraphicsLayer 的 visual bounds,解决 layer 被提升为 offscreen buffer 后,内容容易因为实际 measured bounds 而被裁掉的问题,这个对于 blur、shadow、复杂 graphics layer 会比较有用。
另外还有个坑,但Compose 1.12 的 compileSdk 已更新到 API 37 / Android 17,明确要求最低 AGP 9.2.0,也就是高版本 AGP 跑不掉了,如果你想升级:
所以,准备好了吗?吃上 AGP 9.2 的 Compose 1.12 升级。