A
第 32 章JAVA40 分钟

深色模式与主题

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() { /* ... */ }
}

常见坑与最佳实践

  1. 基于非 DayNight 主题:很多人写主题时 parent="Theme.Material3.Light",导致系统切到深色也不生效。要响应系统深色,基础主题必须带 DayNight。
  2. setDefaultNightMode 调用时机错误:放在 setContentView 之后才调用,本次创建的界面不会重建。必须放在 super.onCreate 之前或调用 recreate()。
  3. 混用 AppCompat 主题和 Material3:Material3 主题基于 AppCompat,AppCompat 的 setDefaultNightMode 同样可用,但用 androidx.appcompat 组件时若误用 framework 主题会丢失部分回调。
  4. forceDarkAllowed 误用:动态生成的图标在 Force Dark 下可能变成“黑底黑字”。需要把这些 View 的 setForceDarkAllowed(false) 关掉,或让父容器接管。
  5. 动态颜色与品牌色冲突:Android 12 设备上的动态颜色可能让品牌 Logo 偏色。重要品牌元素(Logo、二维码)建议硬编码颜色,不参与 DynamicColors。
  6. 主题切换后界面状态丢失:recreate() 会让 Activity 重建,EditText 内容、滚动位置可能丢失。可以用 ViewModel 持久化状态,或对 Activity 声明 android:configChanges="uiMode" 自行处理。
  7. 资源未提供 values-night:在系统深色下找不到对应资源会回退到亮色,造成对比度低(黑字黑底)。每个有色资源都必须提供 values-night 版本。
  8. 过度依赖系统值:直接读 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 布局支持、日期与数字的本地化格式化。