用途
有些界面在构造上就是深色的:它们绘制在全幅艺术图之上,文字为白色,必须忽略浅色主题。该技能提供的机制是仅为该子树重新提供主题,使这一部分保持深色,而应用其余部分继续跟随用户的浅色主题。
主要能力
- 通过
ForceDarkContent组合函数,为子树重新提供MaterialTheme配色方案、排版以及若干 CompositionLocal(LocalForceDarkText、LocalIsDarkTheme、LocalContentColor、LocalAppColors)。 - 强制深色配色方案在应用主题处只解析一次,并通过
staticCompositionLocalOf<ColorScheme?> { null }发布;null 默认值表示“在应用主题之外”,消费方可回退到MaterialTheme.colorScheme。 - 在导航目的地处包裹,而非在屏幕组合函数内部,从而覆盖该屏幕的顶栏、加载与错误状态及其承载的内容。
使用方式
示例代码(资料原文,未改动):
@Composable
fun ForceDarkContent(content: @Composable () -> Unit) {
val darkScheme = LocalForcedDarkColorScheme.current ?: MaterialTheme.colorScheme
MaterialTheme(colorScheme = darkScheme, typography = typo(darkScheme, forceDark = true)) {
CompositionLocalProvider(
LocalForceDarkText provides true,
LocalIsDarkTheme provides true,
LocalContentColor provides darkScheme.onSurfaceVariant,
LocalAppColors provides DarkAppColors,
content = content,
)
}
}
val forcedDarkScheme =
if (isDark) colorScheme
else rememberDynamicColorScheme(seedColor, isDark = true, isAmoled = true, style = TonalSpot)
在导航目的地处包裹:
composable<AlbumDestination> { entry ->
ForceDarkContent { AlbumScreen(browseId = entry.toRoute<AlbumDestination>().browseId, …) }
}
适用场景
- 绘制在全幅深色艺术图之上、需要白色文字并忽略浅色主题的界面。
- 需要让某个界面子树保持深色,而应用其余部分跟随用户浅色主题的场景。
限制与注意事项
- 仅设置文字颜色标志不够:只影响排版的布尔值只会给
Text重新着色,图标、按钮、分隔线及其他 Material 默认组件读取的是LocalContentColor和MaterialTheme.colorScheme,在浅色主题下会变成浅底深字、在艺术图上呈灰色。整个配色方案才能一次性修正子树。 - 强制配色方案应在主题处解析,而非逐屏解析:由种子派生的配色方案是真实开销,各屏自行构建会彼此漂移。
- 包裹列表是注册列表,会漂移:资料提到此处三个文件中有九处包裹点,没有任何东西把它们与需要包裹的屏幕绑定;新增沉浸式屏幕在有人记得该文件存在之前是正确的,且只在浅色主题下失败。建议让成员资格可从屏幕推导(任何从提取调色板绘制背景的屏幕都应列入),使配对可被 grep,并在包裹处写明原因。
- 弹窗与对话框会继承:它们在自己的平台窗口中渲染,但宿主视图会拿到调用方组合作为父组合上下文。资料称已对照编译后的库而非文档核实:Compose UI 的 Android 产物中
PopupLayout.setContent(CompositionContext, content)先调用setParentCompositionContext(ctx),DialogWrapper暴露同样的双参数setContent,Material 3 的ModalBottomSheetDialogLayout亦然;上下文来自调用方的rememberCompositionContext()。因此每个 CompositionLocal 都会跨越窗口边界,弹窗颜色应读取该标志而非硬编码深色。 - 不会继承的是非后代,有两种形态:一是自行作为根的 composition(第二个 activity 或桌面窗口有自己的
setContent、widget,或未设置父组合上下文而 inflate 的 view,此时运行时从窗口解析父级AbstractComposeView.resolveParentCompositionContext(),你的 local 是默认值);二是导航宿主的兄弟节点(在NavHost旁声明的覆盖层,如常驻迷你或展开播放器、全局 snackbar 宿主),它们在同一 composition 中却看不到在目的地提供的 local,因为提供是向下流动的。每个这样的根或兄弟节点都要自行重新提供主题。 - 不要在子树内重新询问系统:调用
isSystemInDarkTheme()用于模糊色调或系统栏外观的 effect 会与强制子树不一致;应把决定发布为 local(LocalIsDarkTheme)并在各处读取,包括包裹器。 - 强制深色不是无障碍深色模式:屏幕是深色是因为其背后的内容,而非用户要求;绝不能让该标志进入设置、持久化或系统栏外观。
- 自定义 token local 也必须重新提供:Material 配色方案之外的任何东西(品牌状态色、微光色调、覆盖层)都有自己的 local 和明暗配对,需显式交出。
验证方法
资料给出以下验证方式:
- 每个包裹点对照每个需要它的屏幕——比较屏幕名而非路径:包裹点在目的地,调色板调用在屏幕文件内,两个列表不共享路径。第二个列表中的每个名称都必须被第一个覆盖(此处为 9 对 15),除辅助函数以及上述必须自行重新提供主题的根和兄弟节点外;打开它们检查:
grep -rn --include='*.kt' -A 3 "ForceDarkContent {" . | grep -v '/build/' | grep -oE "[A-Za-z]+(Screen|ScreenContent|Player)\(" | sort -u
grep -rln --include='*.kt' "toImmersiveBackground\|rememberPaletteState" . | grep -v '/build/' | xargs -n1 basename | sort
- 平台代码直接询问系统而非读取已发布的 local——每处命中都需要理由:
grep -rn --include='*.kt' "isSystemInDarkTheme()" . | grep -v '/build/'
- 对照缓存的库确认弹窗继承的说法,只读、无需构建:
AAR=$(find ~/.gradle/caches/modules-2 -name '*.aar' -path '*ui-android*' | sort -V | tail -1) \
&& echo "$AAR" && D=$(mktemp -d) && unzip -oq "$AAR" classes.jar -d "$D" \
&& unzip -oq "$D/classes.jar" -d "$D/cls" \
&& javap -c -p -classpath "$D/cls" androidx.compose.ui.window.PopupLayout \
| grep -A4 "setContent(androidx.compose.runtime.CompositionContext"
预期 invokevirtual … setParentCompositionContext 是方法体中的第一个调用。sort -V 选取最新缓存版本,不一定是构建实际解析的版本——读取回显的路径(普通 sort 会把 1.8.3 排在 1.12.0 前面)。可换成针对 material3-android AAR 的 androidx.compose.material3.ModalBottomSheetDialogLayout。
- 肉眼验证——能抓住“仅文字标志”失败:在浅色主题下打开沉浸式屏幕,看图标和禁用状态而非标题文字;然后从该屏幕打开一个弹窗,再从普通屏幕打开同一个弹窗,确认二者不同。