上一篇聊了通知体系,这篇聊一个 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 提供了一套完整的语义化颜色属性:
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 引用,不需要资源文件。
暗色模式的坑
图片和图标的暗色适配。白底图标在暗色模式下看不见。用
tint属性着色,或提供values-night版本的 drawable。WebP/JPG 图片无法 tint,需要准备暗色版本。WebView 适配。WebView 不会跟随系统暗色模式。API 33+ 可以用
WebSettings.setAlgorithmicDarkeningAllowed(true),低版本需要注入 CSS。状态栏图标颜色。暗色模式下状态栏背景变暗,图标要改成白色:
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !darkTheme
}
Splash Screen。Android 12 的 SplashScreen 也需要适配暗色,在
values-night里提供暗色版本的windowBackground。
小结
主题系统的核心是语义化——用 ?attr/colorXxx 引用而不是硬编码颜色。这样亮暗模式自动切换,Dynamic Color 也能生效。Material 3 的 Container 颜色和 Dynamic Color 是趋势,新项目直接上 M3,存量项目按模块逐步迁移。
下一篇对比 Coil 和 Glide,聊聊图片加载框架的选择。