24、SystemUI的暗黑模式(Dark Mode):UiModeManager的监听,SystemUI各组件暗黑模式的适配。

暗黑模式,现在基本是每个App的标配了。但SystemUI的暗黑模式,跟普通App可不太一样。普通App只需要监听个Configuration变化,或者用AppCompatDelegate.setDefaultNightMode()就完事了。SystemUI呢?它运行在系统进程里,面对的是整个系统的UI——状态栏、通知栏、锁屏、导航栏……每个组件都有自己的生命周期和绘制逻辑。

说白了,SystemUI的暗黑模式,是一场「多组件协同的换肤手术」。今天我就带你看看,这场手术到底怎么做的。

UiModeManager:系统暗黑模式的「总开关」

SystemUI不直接监听Configuration,而是通过UiModeManager来获取暗黑模式的状态。为什么?因为UiModeManager提供了更细粒度的控制——比如你可以设置夜间模式为MODE_NIGHT_AUTO,让系统根据时间自动切换。

我个人习惯在SystemUI的SystemUIApplication或者核心的Dependency注入中,持有一个UiModeManager的引用。然后通过UiModeManager.getNightMode()获取当前模式,再通过OnConfigurationChangedListener或者自定义的广播来监听变化。

// 获取UiModeManager
UiModeManager uiModeManager = context.getSystemService(UiModeManager.class);
int currentMode = uiModeManager.getNightMode();

// 监听暗黑模式变化
Configuration config = context.getResources().getConfiguration();
int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
switch (currentNightMode) {
    case Configuration.UI_MODE_NIGHT_YES:
        // 暗黑模式
        break;
    case Configuration.UI_MODE_NIGHT_NO:
        // 亮色模式
        break;
    case Configuration.UI_MODE_NIGHT_UNDEFINED:
        // 未定义
        break;
}
小技巧: 我在项目中遇到过,有些设备在切换暗黑模式时,Configuration变化会触发多次回调。建议在监听回调里加一个防抖处理,或者用Handler延迟一下,避免频繁刷新UI。

SystemUI各组件如何适配暗黑模式?

SystemUI的组件很多,但适配暗黑模式的思路是统一的:用ThemeOverlay,而不是硬编码颜色。每个组件都定义了自己的ThemeOverlay,暗黑模式下自动切换。

1. 状态栏(StatusBar)

状态栏的暗黑模式,主要影响的是图标颜色和背景。亮色模式下,图标是深色的;暗黑模式下,图标是白色的。SystemUI通过StatusBarIconController来管理图标颜色,它会监听暗黑模式变化,然后调用setIconTint()更新所有图标。

// StatusBarIconController中的关键代码
private void updateIconTint() {
    boolean isDark = mNightMode == Configuration.UI_MODE_NIGHT_YES;
    int tintColor = isDark ? Color.WHITE : Color.BLACK;
    for (StatusBarIconView icon : mIconViews) {
        icon.setImageTintList(ColorStateList.valueOf(tintColor));
    }
}
避坑指南: 我曾经在适配状态栏暗黑模式时,发现某些第三方App的通知图标在暗黑模式下看不清。原因是这些图标本身是白色的,在暗黑模式下再叠加白色tint,就糊了。后来我加了一个判断:如果图标本身是单色且亮度较高,就不叠加tint。

2. 通知栏(NotificationPanel)

通知栏的暗黑模式适配,核心是NotificationThemeManager。它负责在暗黑模式切换时,重新加载通知的布局和颜色。通知的RemoteViews本身是支持暗黑模式的,但SystemUI需要确保自己的背景、分割线、文字颜色都跟着变。

我记得在Android 12上,Google引入了NotificationStyle的暗黑模式适配,但早期版本需要手动处理。比如通知的setBackgroundColor(),在暗黑模式下要换成深色背景。

// NotificationThemeManager中的适配逻辑
private void applyDarkTheme(boolean isDark) {
    int bgColor = isDark ? 0xFF1C1C1E : 0xFFFFFFFF;
    int textColor = isDark ? 0xFFFFFFFF : 0xFF000000;
    // 更新通知列表的背景
    mNotificationList.setBackgroundColor(bgColor);
    // 更新所有通知项的文本颜色
    for (NotificationViewHolder holder : mViewHolders) {
        holder.titleView.setTextColor(textColor);
        holder.bodyView.setTextColor(textColor);
    }
}

3. 锁屏(Keyguard)

锁屏的暗黑模式,其实比较特殊。因为锁屏本身通常就是深色背景,所以暗黑模式对锁屏的影响不大。但锁屏上的时钟、通知、解锁提示等文字颜色,需要跟着模式走。

嗯,这里要注意:锁屏的暗黑模式切换,不能直接刷新UI,因为锁屏界面可能正在显示动画或者用户正在输入密码。我建议的做法是:在锁屏的onConfigurationChanged()中,只更新颜色值,不重建布局。等用户解锁后,再重新加载完整的锁屏界面。

4. 导航栏(NavigationBar)

导航栏的暗黑模式,主要影响的是三个虚拟按键的颜色。亮色模式下,按键是深色的;暗黑模式下,按键是白色的。SystemUI通过NavigationBarController来管理按键颜色,它会监听暗黑模式变化,然后调用updateKeyColor()更新。

// NavigationBarController中的颜色更新
private void updateKeyColor(boolean isDark) {
    int color = isDark ? Color.WHITE : Color.BLACK;
    mBackButton.setColorFilter(color);
    mHomeButton.setColorFilter(color);
    mRecentButton.setColorFilter(color);
}
注意: 导航栏的暗黑模式适配,有一个常见的坑:当用户开启「自动亮度」时,导航栏的颜色可能会跟系统UI的暗黑模式不同步。这是因为自动亮度会触发Configuration变化,但uiMode可能没变。我建议在导航栏的颜色更新逻辑中,同时监听uiModebrightness的变化。

暗黑模式适配的「统一入口」

SystemUI的暗黑模式适配,并不是每个组件各自为战。它有一个统一的入口——DarkModeController。这个控制器负责监听UiModeManager的变化,然后通知所有注册的组件。

说白了,这就是一个观察者模式。每个组件在初始化时,向DarkModeController注册一个回调。当暗黑模式变化时,DarkModeController遍历所有回调,通知它们更新。

// DarkModeController的核心逻辑
public class DarkModeController {
    private List<DarkModeCallback> mCallbacks = new ArrayList<>();
    
    public void registerCallback(DarkModeCallback callback) {
        mCallbacks.add(callback);
    }
    
    public void onDarkModeChanged(boolean isDark) {
        for (DarkModeCallback callback : mCallbacks) {
            callback.onDarkModeChanged(isDark);
        }
    }
    
    public interface DarkModeCallback {
        void onDarkModeChanged(boolean isDark);
    }
}

这样设计的好处是:每个组件只需要关心自己的适配逻辑,不需要知道其他组件的存在。而且,如果以后新增一个组件,只需要实现DarkModeCallback接口,然后注册到DarkModeController就行了。

暗黑模式适配的「性能优化」

暗黑模式切换时,SystemUI的所有组件都会刷新。如果处理不好,很容易出现卡顿。我总结了几条优化经验:

  • 批量更新: 不要每个组件单独刷新,而是等所有组件都准备好后,统一刷新。可以用Choreographer或者View.postOnAnimation()来同步。
  • 避免重复计算: 暗黑模式切换时,很多颜色值是可以缓存的。比如Color.WHITEColor.BLACK,直接使用常量,不要每次都计算。
  • 延迟加载: 对于不在当前屏幕上的组件(比如折叠状态下的通知),可以延迟刷新。等用户展开时再更新。
我的经验: 我曾经在SystemUI的暗黑模式适配中,遇到一个性能问题:每次切换暗黑模式,状态栏的图标都会闪烁一下。后来发现是StatusBarIconController在更新图标颜色时,没有使用animate(),而是直接setColorFilter()。加上animate()后,闪烁问题就解决了。

SVG:SystemUI暗黑模式适配架构图

SystemUI暗黑模式适配架构 UiModeManager DarkModeController StatusBar 图标颜色、背景 NotificationPanel 背景、文字颜色 Keyguard 时钟、通知颜色 NavigationBar 按键颜色 核心思路:ThemeOverlay + 观察者模式 每个组件只关心自己的适配逻辑,通过DarkModeController统一调度

总结

SystemUI的暗黑模式适配,说白了就是:一个总开关(UiModeManager),一个统一入口(DarkModeController),多个组件各自适配。每个组件通过ThemeOverlay和观察者模式,实现暗黑模式的平滑切换。

你想想看,如果每个组件都自己去监听Configuration变化,那代码得多乱?而且很容易出现不同步的问题。所以,统一入口的设计,是SystemUI暗黑模式适配的关键。

嗯,最后再提醒一句:暗黑模式适配,不只是改颜色。还要考虑性能、动画、第三方兼容性。我在项目中踩过的坑,今天都分享给你了,希望能帮你少走弯路。


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