ViewCompose Theming
1. 文档定位
本文档是主题系统规范版,定义:
- 主题模型边界
- 默认值解析链路
- 局部覆盖规则
- 新增主题能力时的落地约束
历史长版见:
2. 当前主题模型
UiThemeTokens 当前核心字段保持为:
colorsstateColorstypographyshapescontrolsoverlaysmetadata
关键原则:
- 顶层主题只承载语义 token,不承载每个组件的完整 resolved style。
- 组件默认值在
Defaults层按需从Theme派生,不做全量预计算。 - 组件显式参数优先级高于主题默认值。
当前 token 语义补充:
colors同时承载基础色、on*前景色、*Container容器色、轮廓色、逆表面色与 ripple。stateColors承载文本、普通控件、激活控件和交互高亮的 default/disabled/pressed/focused/checked/selected 状态。typography只保留 tieredtitle*/body*/label*作为唯一主入口。shapes只保留语义化small / medium / large三级形状作为唯一主入口;每个形状完整表达四角、圆角/切角与绝对/百分比尺寸。controls仍是框架自有尺寸 token,不承诺与 Android 原主题系统一一对齐。overlays当前由语义 token 承载跨组件蒙层配置。metadata标记 token 来源、深色状态与配置修订号,用于生命周期刷新和诊断,不参与组件默认值推导。
2.3 Token 使用闭环
公开 token 不允许长期停留在“已定义但无消费”的状态。当前规则固定为二选一:
- 至少被一个 core defaults/composite 默认值明确消费。
- 被列入 whitelist,并在文档中说明原因。
当前 whitelist 仅保留暂时没有核心组件直接消费的 reserved semantic palette:
- 扩展表面:
onBackground / surfaceDim / surfaceBright / surfaceContainer* - 第三强调色:
tertiary / onTertiary / tertiaryContainer / onTertiaryContainer - 反色与蒙层:
inversePrimary / scrim / surfaceTint - 业务语义:
success / warning / info
说明:本轮不强绑到现有核心组件,避免为了提高使用率污染语义。
为防止回流,仓库有 ThemeTokenUsageAuditTest 守卫:
- 新增 token 时,若未消费也未加入 whitelist,测试必须直接失败。
- defaults 若从语义 token 回退到旧 alias,也会在审计时暴露。
2.1 语义主入口规则
主题 token 扩展默认采用“语义主入口 + 一次性收口”:
- 新字段一旦成为主语义入口,defaults 与 demo 必须同轮迁移。
- 若旧字段只是历史别名,应在收口轮次直接移除,不继续长期并存。
- 文档必须写明哪些字段属于正式语义入口,哪些字段仅是 reserved token。
- 新增默认值逻辑只允许读取正式语义字段,不允许引入别名回流。
2.2 硬编码禁用清单
以下语义色禁止在 Defaults 里直接写字面量,必须走 Theme.colors:
- 错误态(如
0xFFB3261E)统一使用Theme.colors.error。 - 徽标/提醒色统一使用
Theme.colors.error或其他语义色,不允许组件私有常量重复声明。 - 语义色文本前景统一通过
contentColorFor(semanticColor)推导,不手写黑白常量。
3. 默认值链路
标准链路必须保持:
Theme -> Defaults -> NodeSpec -> Renderer
约束:
- 不把主题直接变成通用
Modifier。 - 不在 renderer 中写业务语义默认值。
- 不在 DSL 层散落重复主题推导逻辑。
- 复合组件内部文本必须把完整文本样式写入
NodeSpec,不能只下发textSizeSp。 - renderer 只负责应用
NodeSpec中已经解析好的文本样式,不重新发明主题语义。
4. 局部覆盖(Override)规则
局部覆盖能力保留,但必须是稀疏覆盖:
- 只覆盖必要字段
- 未覆盖字段回落到上层主题或默认值
- 覆盖逻辑通过
LocalContext作用域传播 - 对外统一通过
UiLocal/uiLocalOf/ProvideLocal(s)/UiLocals.current使用,避免专用包装 API 漂移
适用场景:
- 局部品牌色/强调色
- 局部文本样式调整
- 单区域对比度或可读性增强
非目标:
- 把 override 做成“每个组件所有字段都能填”的全量配置
- 用 override 替代组件参数
4.1 业务自定义 Local 扩展
当业务侧 token 体系与框架默认主题不一致时,允许按下面方式扩展:
- 在业务模块通过
uiLocalOf { ... }定义自有 token。 - 在局部子树通过
ProvideLocal(...)或ProvideLocals(...)注入。 - 在组件内部通过
UiLocals.current(...)读取。 - 新增 Local 能力时优先复用统一 API,不再新增专用
ProvideXxx包装。
边界约束:
- 业务 Local 只承载语义值,不承载 renderer 平台实现细节。
- Local 作用域恢复与 snapshot 传播语义必须保持(lazy/overlay 不回退)。
5. Android Bridge 边界
Android 主题桥接只做“平台语义到框架语义”的映射,内部固定为:
AndroidThemeSnapshotReader -> ThemeTokenMapper -> UiThemeTokens
其中:
SnapshotReader负责批量读取 Android / AppCompat / Material 主题字段。ThemeTokenMapper负责把平台字段映射到框架 token,并处理 fallback。- bridge 不直接产出组件级默认值,不绕过
Defaults层。 - Android
ComponentActivity/Fragment.setUiContent默认解析并提供 Android Theme;根容器、框架原生 View、AndroidView与 Overlay 共用同一个解析上下文。 UiTheme(androidContext = ...)默认在平台支持时套用 Material 动态色;可通过AndroidDynamicColorPolicy.Disabled显式关闭。- 组合内使用 Android 主题时会监听配置变化并重新读取 token;离开组合后注销回调,
metadata.revision随刷新递增。 - 运行时调用
setTheme/applyStyle后,可把AndroidThemeRefreshController传给setUiContent,再在主线程调用refresh();控制器会重新解析动态色上下文并触发主题子树刷新。
当前 bridge 覆盖矩阵:
colors- 已桥接:
background / onBackground / surface / surfaceVariant / primary / secondary / tertiary / error - 已桥接:
surfaceDim / surfaceBright / surfaceContainerLowest/Low/Container/High/Highest - 已桥接:
onPrimary / onSecondary / onTertiary / onError - 已桥接:
primaryContainer / secondaryContainer / tertiaryContainer / errorContainer - 已桥接:
onPrimaryContainer / onSecondaryContainer / onTertiaryContainer / onErrorContainer - 已桥接:
outline / outlineVariant / inverseSurface / inverseOnSurface / inversePrimary - 已桥接:
onSurface / onSurfaceVariant - 已桥接:
ripple(优先读colorControlHighlight) surfaceTint按 Material 3 颜色角色回落到primary,不再错误借用 AppCompatcolorAccent
- 已桥接:
stateColors- 已桥接:
android:textColorPrimary / textColorSecondary - 已桥接:AppCompat
colorControlNormal / colorControlActivated / colorControlHighlight - 标准状态:
disabled / pressed / focused / checked / selected
- 已桥接:
typography- 已桥接:Material 3
textAppearanceTitle*/Body*/Label* - fallback:旧 Android
textAppearanceLarge/Medium/Small - 已桥接字段:
fontSizeSp / fontWeight / fontFamily / letterSpacingEm / lineHeightSp / includeFontPadding
- 已桥接:Material 3
shapes- 已桥接:
shapeAppearanceSmallComponent / Medium / Large - 已桥接:四角独立尺寸、
rounded/cutcorner family、dimension/fraction corner size - Android 的物理 left/right 会按当前布局方向转换为框架的逻辑 start/end
- 已桥接:
overlays- 已桥接:
android:backgroundDimAmount -> scrimOpacity
- 已桥接:
controls- 当前不做主题级强桥接,继续走 framework defaults
- 原因:Android 原主题系统没有与
compact / medium / large一一对应的统一来源
不做:
- 在 bridge 层写组件业务默认值
- 在 bridge 层引入组件级条件分支
- 为了“看起来全覆盖”而猜测性映射控件尺寸
实现约束:
- bridge 的 fallback 必须显式落到
UiThemeDefaults.light/dark(),禁止散落字面量。 - 新增桥接字段时,必须同时定义“读取来源 + fallback 规则 + token 归属”。
- bridge 新能力若改变可视结果,必须补
AndroidThemeBridgeTest或 Android 侧桥接测试。
主动刷新示例:
val themeRefreshController = AndroidThemeRefreshController()
setUiContent(themeRefreshController = themeRefreshController) {
// content
}
setTheme(R.style.AppTheme_Alternate)
themeRefreshController.refresh()
6. 与组件和 Modifier 的边界
- 主题负责默认值来源
- 组件参数负责语义表达
Modifier负责通用外层修饰
对应规范:
7. 新增主题能力的必经清单
新增主题字段或覆盖能力时,至少完成:
- 模型归属判断:
tokens/defaults/ 组件参数 - 优先级规则定义:默认值与显式参数冲突时谁生效
- renderer 验证:样式变化可触发预期 patch/rebind
- demo 验证:Light/Dark + 局部覆盖场景
- 测试补齐:单测或 instrumentation 至少覆盖一种回归路径
当前主题语义的权威 demo 验证入口为 Diagnostics -> 主题诊断。Foundations 中的 theme/overrides/typography 页面继续保留为教学与示例入口,不承担最终人工回归口径。
8. 当前阶段重点
- 保持主题模型稳定,不回退到“组件全量 token 预计算”。
- 动态色、完整 shape 映射与配置生命周期已落地;继续补多窗口/厂商主题的设备矩阵。
- 与
ROADMAP中 overlay、input、容器场景联动完善主题回归。
路线图见: