深色模式与主题
Android 主题系统:Material 3 主题、颜色资源、Theme.Material3.DayNight、Force Dark、Android 12+ 动态颜色(Dynamic Color),以及主题切换的持久化方案。
学习目标
- 理解 Android 主题与样式系统
- 能够定义颜色资源并按夜色模式提供替代值
- 掌握 DayNight 主题与 AppCompatDelegate 设置模式
- 了解 Force Dark 兼容老版本深色
- 能够集成 Android 12+ 动态颜色并实现主题切换持久化
学习目标
- 理解 Android 主题与样式系统;
- 能够定义颜色资源并按夜色模式提供替代值;
- 掌握 DayNight 主题与
AppCompatDelegate设置模式; - 了解 Force Dark 兼容老版本深色;
- 能够集成 Android 12+ 动态颜色并实现主题切换持久化。
深色模式与主题
主题与样式的区分
- Theme(主题):作用于整个 Activity / Application,定义
colorPrimary、colorOnPrimary、windowBackground等系统级属性; - Style(样式):作用于单个 View,定义
textSize、textColor、padding等具体外观。
两者定义方式相同(XML in res/values/),区别在于使用位置:
<!-- 在 AndroidManifest.xml 应用主题 -->
<application
android:theme="@style/Theme.MyApp">
<activity android:theme="@style/Theme.MyApp.Detail" />
</application>
<!-- 在布局中应用样式 -->
<TextView
android:theme="@style/TextAppearance.Headline"
style="@style/Widget.MyApp.TextView.Title" />
Material 3 主题
Android 官方 Material 3(Material You)主题引入了色板系统:每个主题定义若干基础颜色,由 Material 组件自动派生出 On 色、Container 色、Error 色等:
<!-- res/values/themes.xml -->
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<!-- 主色:按钮、App Bar 等使用 -->
<item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorOnPrimary">@color/md_theme_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<!-- 次色:FAB、辅助控件 -->
<item name="colorSecondary">@color/md_theme_secondary</item>
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
<!-- 第三色:可选强调 -->
<item name="colorTertiary">@color/md_theme_tertiary</item>
<!-- 错误色 -->
<item name="colorError">@color/md_theme_error</item>
<item name="colorOnError">@color/md_theme_onError</item>
<!-- 背景 / 表面 -->
<item name="android:colorBackground">@color/md_theme_background</item>
<item name="colorSurface">@color/md_theme_surface</item>
<item name="colorOnSurface">@color/md_theme_onSurface</item>
</style>
颜色资源 res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 亮色主题 -->
<color name="md_theme_primary">#3F51B5</color>
<color name="md_theme_onPrimary">#FFFFFF</color>
<color name="md_theme_primaryContainer">#E8EAF6</color>
<color name="md_theme_onPrimaryContainer">#1A237E</color>
<color name="md_theme_secondary">#5C6BC0</color>
<color name="md_theme_onSecondary">#FFFFFF</color>
<color name="md_theme_tertiary">#FFB74D</color>
<color name="md_theme_error">#B00020</color>
<color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_background">#FAFAFA</color>
<color name="md_theme_surface">#FFFFFF</color>
<color name="md_theme_onSurface">#212121</color>
</resources>
夜色主题资源 res/values-night/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 深色主题:饱和度更低,对比足够 -->
<color name="md_theme_primary">#8E9FFF</color>
<color name="md_theme_onPrimary">#002184</color>
<color name="md_theme_primaryContainer">#2A3A9C</color>
<color name="md_theme_onPrimaryContainer">#DCE2FF</color>
<color name="md_theme_secondary">#B0BCFF</color>
<color name="md_theme_onSecondary">#00227A</color>
<color name="md_theme_tertiary">#FFB74D</color>
<color name="md_theme_error">#FFB4A9</color>
<color name="md_theme_onError">#690005</color>
<color name="md_theme_background">#121212</color>
<color name="md_theme_surface">#1F1F1F</color>
<color name="md_theme_onSurface">#E6E1E5</color>
</resources>
values-night 资源限定符会在系统开启深色模式时自动生效,无需手写判断逻辑。
DayNight 主题
Theme.Material3.DayNight 是支持夜色切换的主题:
<!-- res/values/themes.xml -->
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<!-- ... -->
</style>
要让 Activity / Application 跟随系统深色模式,所有基主题都基于 DayNight 才会自动响应。如果基于非 DayNight 主题(如 Theme.Material3.Light),即便系统切换到深色模式也不会生效。
通过 AppCompatDelegate 切换模式
AppCompatDelegate.setDefaultNightMode(int) 设置 App 范围内深色模式:
import androidx.appcompat.app.AppCompatDelegate;
public class ThemeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
applyStoredMode();
setContentView(R.layout.activity_theme);
findViewById(R.id.btn_follow_system).setOnClickListener(v -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
persistMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
});
findViewById(R.id.btn_light).setOnClickListener(v -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
persistMode(AppCompatDelegate.MODE_NIGHT_NO);
});
findViewById(R.id.btn_dark).setOnClickListener(v -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
persistMode(AppCompatDelegate.MODE_NIGHT_YES);
});
}
private void applyStoredMode() {
SharedPreferences sp = getSharedPreferences("theme_prefs", MODE_PRIVATE);
int mode = sp.getInt("night_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
AppCompatDelegate.setDefaultNightMode(mode);
}
private void persistMode(int mode) {
getSharedPreferences("theme_prefs", MODE_PRIVATE)
.edit().putInt("night_mode", mode).apply();
recreate(); // 重新创建 Activity 以立即应用主题
}
}
AppCompatDelegate 支持四种模式:
| 常量 | 含义 |
|---|---|
MODE_NIGHT_NO |
永远浅色 |
MODE_NIGHT_YES |
永远深色 |
MODE_NIGHT_FOLLOW_SYSTEM |
跟随系统 |
MODE_NIGHT_AUTO_BATTERY |
系统省电模式开启时深色(API 23+) |
setDefaultNightMode 是全局的、进程级状态,调用后会自动重建所有非 configChanges 拦截 uiMode 的 Activity;如果 Activity 已声明 android:configChanges="uiMode",需要手动 recreate() 或自己更新界面。
Force Dark:兼容老应用
对于尚未提供 values-night 资源的老应用,Android 10+ 引入 Force Dark——系统自动反转浅色主题为深色近似。
启用:
<style name="Theme.MyApp" parent="Theme.Material3.DayNight">
<item name="android:forceDarkAllowed">true</item>
</style>
forceDarkAllowed仅在 Android 10+ 生效;- 已经是
DayNight的应用通常不需要 Force Dark; - 部分 View(如自定义绘制图片的 View)需要在代码中
setForceDarkAllowed(false)排除。
Force Dark 是过渡方案,新项目应直接提供 values-night 资源,不依赖 Force Dark。
动态颜色(Android 12+ 动态取色)
Android 12(API 31)引入 Material You 动态颜色——根据用户壁纸生成调色板,应用到支持的应用。
依赖
dependencies {
implementation "com.google.android.material:material:1.12.0"
}
启用 DynamicColors
import com.google.android.material.color.DynamicColors;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 在 Android 12+ 设备上自动应用动态颜色
DynamicColors.applyToActivitiesIfAvailable(this);
}
}
需要在 AndroidManifest.xml 注册 Application:
<application
android:name=".MyApplication"
android:theme="@style/Theme.MyApp">
<!-- ... -->
</application>
DynamicColors.applyToActivitiesIfAvailable 会通过 ActivityLifecycleCallbacks 在每个 Activity 的 onCreate 前 hook 主题,使其基于 Wallpaper 色板。低于 Android 12 的设备回退到 values/colors.xml 的默认色板。
自定义动态颜色回调
DynamicColorsOptions options = new DynamicColorsOptions.Builder()
.setPrecondition((activity, theme) -> {
// 仅特定 Activity 应用动态颜色
return activity instanceof MainActivity;
})
.build();
DynamicColors.applyToActivitiesIfAvailable(this, options);
主题切换持久化
完整示例:在启动时读取用户选择,按需应用,并提供切换 UI:
public class ThemeHelper {
private static final String PREFS_NAME = "theme_prefs";
private static final String KEY_NIGHT_MODE = "night_mode";
private final SharedPreferences prefs;
public ThemeHelper(Context context) {
this.prefs = context.getApplicationContext()
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public int getMode() {
return prefs.getInt(KEY_NIGHT_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
public void setMode(int mode) {
prefs.edit().putInt(KEY_NIGHT_MODE, mode).apply();
AppCompatDelegate.setDefaultNightMode(mode);
}
public boolean isDark(Context context) {
int mode = new ResourcesConfiguration(context).getUiNightMode();
return mode == Configuration.UI_MODE_NIGHT_YES;
}
}
ResourcesConfiguration 是辅助类:
public class ResourcesConfiguration {
private final Context context;
public ResourcesConfiguration(Context context) {
this.context = context.getApplicationContext();
}
public int getUiNightMode() {
return context.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
}
}
在 BaseActivity 中统一处理:
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeHelper helper = new ThemeHelper(this);
AppCompatDelegate.setDefaultNightMode(helper.getMode());
super.onCreate(savedInstanceState);
}
}
注意:setDefaultNightMode 必须在 super.onCreate 前调用,否则首次创建的 Activity 不会正确应用主题。
监听系统深色模式变化
如果你的 Activity 在 AndroidManifest 声明了 android:configChanges="uiMode",系统不会自动重建,需要在 onConfigurationChanged 中处理:
public class ThemeActivity extends AppCompatActivity {
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
int nightMode = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (nightMode == Configuration.UI_MODE_NIGHT_YES) {
updateIconsForDark();
} else {
updateIconsForLight();
}
}
private void updateIconsForDark() { /* ... */ }
private void updateIconsForLight() { /* ... */ }
}
常见坑与最佳实践
- 基于非 DayNight 主题:很多人写主题时
parent="Theme.Material3.Light",导致系统切到深色也不生效。要响应系统深色,基础主题必须带DayNight。 setDefaultNightMode调用时机错误:放在setContentView之后才调用,本次创建的界面不会重建。必须放在super.onCreate之前或调用recreate()。- 混用 AppCompat 主题和 Material3:
Material3主题基于 AppCompat,AppCompat 的setDefaultNightMode同样可用,但用androidx.appcompat组件时若误用 framework 主题会丢失部分回调。 forceDarkAllowed误用:动态生成的图标在 Force Dark 下可能变成“黑底黑字”。需要把这些 View 的setForceDarkAllowed(false)关掉,或让父容器接管。- 动态颜色与品牌色冲突:Android 12 设备上的动态颜色可能让品牌 Logo 偏色。重要品牌元素(Logo、二维码)建议硬编码颜色,不参与 DynamicColors。
- 主题切换后界面状态丢失:
recreate()会让 Activity 重建,EditText 内容、滚动位置可能丢失。可以用ViewModel持久化状态,或对 Activity 声明android:configChanges="uiMode"自行处理。 - 资源未提供
values-night:在系统深色下找不到对应资源会回退到亮色,造成对比度低(黑字黑底)。每个有色资源都必须提供values-night版本。 - 过度依赖系统值:直接读
Configuration.UI_MODE_NIGHT_MASK在主线程多次调用有性能成本,应缓存结果。
章节小结
本章覆盖了 Android 主题系统:主题(Theme)与样式(Style)的边界、Material 3 色板与基础颜色资源定义、values-night 资源限定符让系统自动选择深浅色、DayNight 主题、AppCompatDelegate.setDefaultNightMode 三种模式切换、Force Dark 兼容老应用、Android 12+ 动态颜色(DynamicColors)以及主题切换的持久化方案。核心要点:所有基主题都基于 DayNight,颜色资源全部提供 values-night 版本,主题状态用 SharedPreferences 持久化并在 super.onCreate 之前应用。
下一章预告
下一章将进入国际化与多语言领域:strings.xml 多语言配置、locale 资源目录(values-zh / values-en)、运行时切换语言(Configuration / ContextWrapper)、RTL 布局支持、日期与数字的本地化格式化。