18、深色模式:适配深色主题、isSystemInDarkTheme 的使用

深色模式,现在基本是每个 App 的标配了。用户喜欢在晚上刷手机时切换到深色背景,眼睛舒服,还能省电(尤其是 OLED 屏幕)。

我个人习惯在项目一开始就把深色模式的支持加进去。别等到后面再改,那会儿改起来就有点痛苦了。Compose 对深色模式的支持其实很友好,比传统 View 系统简单得多。

18.1 深色模式的核心思路

说白了,深色模式就是一套颜色方案。你需要定义两套颜色:一套给浅色主题,一套给深色主题。系统会根据用户的设置自动切换。

在 Compose 里,我们通过 MaterialTheme 来管理主题。它内部有一个 darkColorSchemelightColorScheme。你只需要提供这两套颜色,剩下的交给框架。

嗯,这里要注意:不是所有颜色都要定义两套。有些颜色在深色和浅色下是一样的,比如某些强调色。但背景色、表面色、文字色这些,必须区分开。

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 知识体系结构图

下面这张图总结了深色模式适配的核心流程:

深色模式适配核心流程 isSystemInDarkTheme() 选择颜色方案 lightColorScheme() darkColorScheme() MaterialTheme(colorScheme = ...) 所有 Composable 通过 MaterialTheme.colorScheme 获取颜色,自动适配

18.8 避坑指南

注意: 不要在所有地方都用 isSystemInDarkTheme() 来判断。这个函数只能在 Composable 函数里调用。如果你在 ViewModel 或普通类里需要知道当前主题,应该通过参数传进去。

我曾经犯过一个错误:在 ViewModel 里直接调用了 isSystemInDarkTheme(),结果编译报错。后来我把主题状态作为参数从 Activity 传进去,才解决。

另外,深色模式下的阴影效果要小心。在浅色背景下,阴影是灰色的,但在深色背景下,灰色阴影几乎看不见。我建议你在深色模式下用白色半透明阴影,或者干脆不用阴影,改用轮廓线。

18.9 总结

深色模式适配其实不复杂。核心就三步:

  1. 定义两套颜色方案(浅色和深色)
  2. isSystemInDarkTheme() 判断当前主题
  3. 通过 MaterialTheme 应用对应的颜色方案

然后你的所有 UI 组件都通过 MaterialTheme.colorScheme 获取颜色,这样就能自动适配了。

如果你想让用户手动切换,加一个 Switch 控件,把状态存到 SharedPreferences 里就行。嗯,就这么简单。

核心要点:

  • isSystemInDarkTheme() 是 Compose 提供的系统深色模式检测函数
  • 使用 lightColorScheme()darkColorScheme() 定义两套颜色
  • 所有颜色引用都通过 MaterialTheme.colorScheme,不要硬编码
  • 用户手动切换的偏好需要持久化存储

公众号:蓝海资料掘金营,微信deep3321