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最酷的地方是什么?是它能从用户的壁纸里提取颜色,自动生成一套和谐的调色板。你想想看,每个用户的手机看起来都不一样,这体验多棒。
实现起来其实不复杂。核心就两步:
- 在
AndroidManifest.xml里给Activity加上android:theme="@style/Theme.Material3.DynamicColors.DayNight" - 在代码里调用
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已经帮你定义好了surface、onSurface、surfaceVariant等颜色。你只需要在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。
来看一张我总结的架构图:
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