18、深色模式:适配深色主题、isSystemInDarkTheme 的使用
深色模式,现在基本是每个 App 的标配了。用户喜欢在晚上刷手机时切换到深色背景,眼睛舒服,还能省电(尤其是 OLED 屏幕)。
我个人习惯在项目一开始就把深色模式的支持加进去。别等到后面再改,那会儿改起来就有点痛苦了。Compose 对深色模式的支持其实很友好,比传统 View 系统简单得多。
18.1 深色模式的核心思路
说白了,深色模式就是一套颜色方案。你需要定义两套颜色:一套给浅色主题,一套给深色主题。系统会根据用户的设置自动切换。
在 Compose 里,我们通过 MaterialTheme 来管理主题。它内部有一个 darkColorScheme 和 lightColorScheme。你只需要提供这两套颜色,剩下的交给框架。
嗯,这里要注意:不是所有颜色都要定义两套。有些颜色在深色和浅色下是一样的,比如某些强调色。但背景色、表面色、文字色这些,必须区分开。
18.2 isSystemInDarkTheme 是什么?
isSystemInDarkTheme() 是 Compose 提供的一个 Composable 函数。它会读取系统的深色模式设置,返回一个布尔值。
我在项目中遇到过一个问题:用户明明在系统设置里开了深色模式,但 App 还是浅色的。后来发现是我忘了调用这个函数。你想想看,如果你不告诉 Compose 当前是深色还是浅色,它怎么知道该用哪套颜色呢?
用法很简单:
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
这段代码里,darkTheme 默认就是 isSystemInDarkTheme() 的返回值。如果你想让用户手动切换,也可以把这个参数暴露出去,让用户自己选择。
18.3 定义两套颜色方案
我建议你单独建一个 Theme.kt 文件,专门放主题相关的代码。这样好维护。
先定义浅色主题:
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF1976D2),
onPrimary = Color.White,
primaryContainer = Color(0xFFBBDEFB),
onPrimaryContainer = Color(0xFF004BA0),
secondary = Color(0xFF388E3C),
onSecondary = Color.White,
background = Color(0xFFF5F5F5),
onBackground = Color(0xFF1C1B1F),
surface = Color.White,
onSurface = Color(0xFF1C1B1F),
error = Color(0xFFD32F2F),
onError = Color.White
)
再定义深色主题:
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF90CAF9),
onPrimary = Color(0xFF003C8F),
primaryContainer = Color(0xFF004BA0),
onPrimaryContainer = Color(0xFFBBDEFB),
secondary = Color(0xFF81C784),
onSecondary = Color(0xFF1B5E20),
background = Color(0xFF121212),
onBackground = Color(0xFFE6E1E5),
surface = Color(0xFF1E1E1E),
onSurface = Color(0xFFE6E1E5),
error = Color(0xFFEF9A9A),
onError = Color(0xFF690005)
)
你看,深色主题的背景色是 #121212,这是 Material Design 推荐的深色背景色。文字颜色也变成了浅色,这样才能在深色背景上看得清。
18.4 在 Compose 中使用主题
定义好之后,在 App 的入口处包裹一下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
// 你的页面内容
Greeting("Android")
}
}
}
}
然后在你所有的 Composable 函数里,直接通过 MaterialTheme.colorScheme 获取颜色:
@Composable
fun Greeting(name: String) {
val backgroundColor = MaterialTheme.colorScheme.background
val textColor = MaterialTheme.colorScheme.onBackground
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Text(
text = "Hello $name",
color = textColor,
style = MaterialTheme.typography.headlineMedium
)
}
}
这样做的好处是:你不需要在代码里写任何判断深色还是浅色的逻辑。Compose 会自动根据当前主题切换颜色。
MaterialTheme.colorScheme 里的颜色,你的 UI 会自动适配深色模式。但如果你用了硬编码的颜色(比如 Color.Blue),那它就不会变了。所以,尽量用主题颜色。
18.5 手动切换深色模式
有时候用户不想跟着系统走,他想在 App 里自己选。这个需求很常见。
实现思路很简单:用一个 MutableState 来保存用户的偏好,然后传给 MyAppTheme。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var isDarkTheme by remember { mutableStateOf(false) }
MyAppTheme(
darkTheme = isDarkTheme
) {
// 页面内容
SettingsScreen(
isDarkTheme = isDarkTheme,
onThemeChange = { isDarkTheme = it }
)
}
}
}
}
@Composable
fun SettingsScreen(
isDarkTheme: Boolean,
onThemeChange: (Boolean) -> Unit
) {
// 一个 Switch 控件
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("深色模式")
Spacer(modifier = Modifier.width(8.dp))
Switch(
checked = isDarkTheme,
onCheckedChange = onThemeChange
)
}
}
我曾经踩过一个坑:用户手动切换后,一重启 App 又变回系统默认了。后来我加上了 SharedPreferences 持久化存储,才解决这个问题。嗯,这里要提醒你,用户的偏好一定要存起来。
18.6 深色模式下的图片处理
图片在深色模式下可能会显得太亮。我建议你准备两套图片资源:一套浅色背景用的,一套深色背景用的。
在 Compose 里,你可以通过 isSystemInDarkTheme() 来判断当前主题,然后加载不同的图片:
@Composable
fun LogoImage() {
val isDark = isSystemInDarkTheme()
val imageRes = if (isDark) {
R.drawable.logo_dark
} else {
R.drawable.logo_light
}
Image(
painter = painterResource(id = imageRes),
contentDescription = "Logo"
)
}
如果你的图片是网络图片,可以在加载时加一个半透明黑色遮罩,或者调整亮度。这个就看具体需求了。
18.7 知识体系结构图
下面这张图总结了深色模式适配的核心流程:
18.8 避坑指南
isSystemInDarkTheme() 来判断。这个函数只能在 Composable 函数里调用。如果你在 ViewModel 或普通类里需要知道当前主题,应该通过参数传进去。
我曾经犯过一个错误:在 ViewModel 里直接调用了 isSystemInDarkTheme(),结果编译报错。后来我把主题状态作为参数从 Activity 传进去,才解决。
另外,深色模式下的阴影效果要小心。在浅色背景下,阴影是灰色的,但在深色背景下,灰色阴影几乎看不见。我建议你在深色模式下用白色半透明阴影,或者干脆不用阴影,改用轮廓线。
18.9 总结
深色模式适配其实不复杂。核心就三步:
- 定义两套颜色方案(浅色和深色)
- 用
isSystemInDarkTheme()判断当前主题 - 通过
MaterialTheme应用对应的颜色方案
然后你的所有 UI 组件都通过 MaterialTheme.colorScheme 获取颜色,这样就能自动适配了。
如果你想让用户手动切换,加一个 Switch 控件,把状态存到 SharedPreferences 里就行。嗯,就这么简单。
核心要点:
isSystemInDarkTheme()是 Compose 提供的系统深色模式检测函数- 使用
lightColorScheme()和darkColorScheme()定义两套颜色 - 所有颜色引用都通过
MaterialTheme.colorScheme,不要硬编码 - 用户手动切换的偏好需要持久化存储