23、主题与样式:Material You动态主题、深色模式适配

聊到主题和样式,我猜很多同学第一反应就是「换个颜色嘛」。嗯,这话对了一半。在Android开发里,主题系统其实比你想象的要深得多。尤其是Material You出来之后,整个玩法都变了。

我个人习惯把主题系统分成三层来看:基础样式定义动态主题适配深色模式切换。这三层搞明白了,你的App在视觉上基本就稳了。

23.1 从Style到Theme:别再傻傻分不清

先解决一个老生常谈的问题:Style和Theme到底啥区别?

说白了,Style是给单个View用的,比如一个Button的字体大小、背景色。Theme是给整个页面或应用用的,它定义了一组全局的语义化颜色、字体、形状。

举个例子你就明白了:

<!-- Style:只影响这个Button -->
<style name="MyButtonStyle">
    <item name="android:textSize">16sp</item>
    <item name="android:textColor">@color/white</item>
</style>

<!-- Theme:影响整个Activity -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="colorPrimary">@color/my_primary</item>
    <item name="colorOnPrimary">@color/my_on_primary</item>
</style>

我在项目中遇到过不少新手把主题属性写在Style里,结果发现全局颜色没变。记住:Theme定义调色板,Style消费调色板

23.2 Material You:动态主题的「魔法」

Material You最酷的地方是什么?是它能从用户的壁纸里提取颜色,自动生成一套和谐的调色板。你想想看,每个用户的手机看起来都不一样,这体验多棒。

实现起来其实不复杂。核心就两步:

  1. AndroidManifest.xml里给Activity加上android:theme="@style/Theme.Material3.DynamicColors.DayNight"
  2. 在代码里调用DynamicColors.applyToActivityIfAvailable(this)

嗯,这里要注意:动态主题只在Android 12+上生效。低版本怎么办?我建议你准备一套fallback颜色。

核心思路:DynamicColors库自动生成颜色,同时保留手动配置的兜底方案。

来看一个完整的接入示例:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // 先应用动态主题
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            DynamicColors.applyToActivityIfAvailable(this)
        }
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

你以为这就完了?不,真正的坑在后面。

23.3 深色模式适配:不只是换个背景

深色模式适配,我见过最粗暴的做法就是「把白色背景改成黑色」。结果呢?文字看不清,图片发灰,整个界面像糊了一层灰。

正确的做法是:使用语义化颜色。Material 3已经帮你定义好了surfaceonSurfacesurfaceVariant等颜色。你只需要在themes.xml里分别定义浅色和深色两套值。

我曾经踩过一个坑:自定义View里直接写了Color.WHITE作为背景色。结果深色模式下,白色背景配白色文字,啥也看不见。从那以后,我所有颜色都走主题属性。

避坑指南:不要在代码里硬编码颜色值。永远通过ContextCompat.getColor()或主题属性引用。

正确的深色模式适配,应该像这样:

<!-- values/themes.xml(浅色) -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="colorSurface">#FFFFFFFF</item>
    <item name="colorOnSurface">#FF1C1B1F</item>
</style>

<!-- values-night/themes.xml(深色) -->
<style name="AppTheme" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="colorSurface">#FF1C1B1F</item>
    <item name="colorOnSurface">#FFE6E1E5</item>
</style>

注意看父主题:Theme.Material3.DayNight。这个DayNight后缀是关键,它会根据系统深色模式自动切换。

23.4 动态主题 + 深色模式:组合拳怎么打

这两个功能一起用的时候,有个细节很多人会忽略:动态主题提取的颜色,也要区分浅色和深色

Material You的DynamicColors库其实已经帮你处理了。它会根据当前系统主题,生成对应的浅色/深色调色板。你只需要确保你的主题继承自Theme.Material3.DynamicColors.DayNight

来看一张我总结的架构图:

Material You 主题系统架构 系统壁纸 / 用户选择 DynamicColors 自动提取调色板 浅色主题 colors.xml 深色主题 colors-night.xml 自定义主题覆盖 App 界面:统一、和谐、自适应

23.5 实战:一个完整的主题切换方案

说了这么多理论,咱们来点实际的。我整理了一套我自己项目里用的主题切换方案,你可以直接拿来用。

首先,在build.gradle里引入依赖:

implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.core:core-ktx:1.12.0'

然后,在Application里初始化:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        // 应用动态主题(Android 12+)
        DynamicColors.applyToActivitiesIfAvailable(this)
    }
}

最后,在themes.xml里定义你的颜色:

<resources>
    <style name="AppTheme" parent="Theme.Material3.DynamicColors.DayNight.NoActionBar">
        <!-- 自定义覆盖 -->
        <item name="colorPrimary">@color/my_primary</item>
        <item name="colorOnPrimary">@color/my_on_primary</item>
        <item name="colorSurface">@color/my_surface</item>
        <item name="colorOnSurface">@color/my_on_surface</item>
    </style>
</resources>

小技巧:如果你想让用户手动切换深色模式,可以用AppCompatDelegate.setDefaultNightMode()。但记得把选择存到SharedPreferences里,不然重启就没了。

23.6 避坑总结

最后,我把这些年踩过的坑列出来,你遇到了直接对照:

问题 原因 解决方案
动态主题不生效 Activity没继承DynamicColors主题 检查AndroidManifest和代码调用
深色模式颜色不对 用了硬编码颜色 全部改用主题属性引用
自定义View颜色不随主题变 没在onDraw里读取主题属性 使用ThemeEnforcement或手动获取
低版本设备崩溃 调用了API 31+的方法 加版本判断或使用兼容库

嗯,关于主题和样式,今天就聊到这儿。记住一句话:好的主题系统,用户感觉不到它的存在,但离开它就会觉得不舒服。你想想看,是不是这个理?


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