Administrator
发布于 2026-08-25 / 19 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 35 篇:Android 主题系统:Material 3、Dynamic Color 与暗色模式适配

上一篇聊了通知体系,这篇聊一个 UI 工程师每天都在打交道但很少深入的东西——主题系统。从 Material Design 1 到 3,从硬编码颜色到 Dynamic Color,Android 的主题方案迭代了好几轮。

Material Design 的演进

  • Material 1:Google I/O 2014 发布,引入 elevation、ripple、FAB 等概念。颜色通过 colorPrimary、colorAccent 定义。

  • Material 2:2018 年 Material Components for Android 库发布。支持 DayNight 主题,引入 MaterialShape。

  • Material 3:2021 年发布,也叫 Material You。引入 Dynamic Color、大圆角、更扁平的视觉风格。

从 M2 到 M3 不只是视觉变化,组件 API 也有较大差异。

Dynamic Color

Android 12(API 31)引入了壁纸提取色功能。系统根据用户壁纸自动生成一套调色板,App 可以直接使用:

// 动态颜色获取
val dynamicContext = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    context.createConfigurationContext(
        Configuration().apply {
            setUiMode(UiModeManager.MODE_NIGHT_AUTO)
        }
    )
} else {
    context
}

在 Compose 中更简单:

@Composable
fun getDynamicColorScheme(): ColorScheme {
    val context = LocalContext.current
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        if (isSystemInDarkTheme()) {
            dynamicDarkColorScheme(context)
        } else {
            dynamicLightColorScheme(context)
        }
    } else {
        // 降级到静态配色
        if (isSystemInDarkTheme()) DarkColors else LightColors
    }
}

Dynamic Color 的好处是 App 的配色会跟着用户壁纸变化,视觉一致性更强。但也要注意品牌色:如果你的 App 有强品牌色(比如微信绿、淘宝橙),不要完全依赖 Dynamic Color,应该做一套自己的 brand color 作为覆盖。

View 体系的主题适配

传统 View 体系用 values-night 资源限定符做暗色模式:

res/
  values/
    colors.xml      # 亮色
    themes.xml       # 亮色主题
  values-night/
    colors.xml      # 暗色
    themes.xml       # 暗色主题
<!-- values/themes.xml -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
    <item name="colorPrimary">@color/md_theme_light_primary</item>
</style>

<!-- values-night/themes.xml -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
    <item name="colorPrimary">@color/md_theme_dark_primary</item>
</style>

使用 DayNight 主题父类后,系统切换暗色模式时自动应用 night 资源。

代码中强制切换模式:

AppCompatDelegate.setDefaultNightMode(
    AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)

可选值:MODE_NIGHT_NO(亮色)、MODE_NIGHT_YES(暗色)、MODE_NIGHT_FOLLOW_SYSTEM(跟随系统)、MODE_NIGHT_AUTO_BATTERY(省电模式时暗色)。

主题属性 vs 直接引用颜色

一个常见错误是直接在布局里写颜色:

<!-- 不推荐 -->
<TextView android:textColor="#FF0000" />

<!-- 推荐 -->
<TextView android:textColor="?attr/colorOnSurface" />

用主题属性(?attr/xxx)引用颜色,切换主题时自动变色。Material 3 提供了一套完整的语义化颜色属性:

属性

用途

colorPrimary

主色(按钮、FAB)

colorOnPrimary

主色上的内容色

colorSurface

表面色(Card、Dialog 背景)

colorOnSurface

表面上的内容色

colorError

错误色

colorOutline

轮廓线

colorPrimaryContainer

容器型主色(浅色背景按钮)

Container 颜色是 M3 的新概念,用于填充型组件的背景,比 Primary 更柔和。

Compose 的主题

@Composable
fun MyApp(content: @Composable () -> Unit) {
    val darkTheme = isSystemInDarkTheme()
    val colorScheme = when {
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        content = content
    )
}

Compose 的优势是颜色、字体、形状都在一个 MaterialTheme 里管理,且编译时类型安全。用 MaterialTheme.colorScheme.primary 引用,不需要资源文件。

暗色模式的坑

  1. 图片和图标的暗色适配。白底图标在暗色模式下看不见。用 tint 属性着色,或提供 values-night 版本的 drawable。WebP/JPG 图片无法 tint,需要准备暗色版本。

  2. WebView 适配。WebView 不会跟随系统暗色模式。API 33+ 可以用 WebSettings.setAlgorithmicDarkeningAllowed(true),低版本需要注入 CSS。

  3. 状态栏图标颜色。暗色模式下状态栏背景变暗,图标要改成白色:

WindowCompat.getInsetsController(window, window.decorView).apply {
    isAppearanceLightStatusBars = !darkTheme
}
  1. Splash Screen。Android 12 的 SplashScreen 也需要适配暗色,在 values-night 里提供暗色版本的 windowBackground。

小结

主题系统的核心是语义化——用 ?attr/colorXxx 引用而不是硬编码颜色。这样亮暗模式自动切换,Dynamic Color 也能生效。Material 3 的 Container 颜色和 Dynamic Color 是趋势,新项目直接上 M3,存量项目按模块逐步迁移。

下一篇对比 Coil 和 Glide,聊聊图片加载框架的选择。


评论