最近项目中需要适配暗夜模式。 且需要有亮色,深色及跟随系统的选择项。查了目前市面上的一些实现方案,最终选择了官方的深色主题的方式,之所以选择这种方式,主要基于以下几点:
- 对已有项目的侵入性较低,可维护性高。
- 不影响旧有的开发方式,接入成本低
- 可以实时预览,方便开发。
设置深色主题
为了支持深色主题,首先需要将应用的主题设置为继承自DayNight主题
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
或者也可以使用
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
然后准备暗色资源,包括图片资源,色值等。只需要在res中对需要适配深色主题的目录增加-night目录,如mipmap-night-xxxhdpi,drawable-night,values-night等。然后把对应的深色主题的资源放进去对应目录即可,这样系统就会根据当前的模式加载对应的资源。

接下来就是最重要的修改主题,谷歌提供了几个选项让我们可以设置应用主题,分别是
/**
* Mode which uses the system's night mode setting to determine if it is night or not.
*
* @see #setLocalNightMode(int)
*/
public static final int MODE_NIGHT_FOLLOW_SYSTEM = -1;
/**
* Night mode which uses always uses a light mode, enabling {@code notnight} qualified
* resources regardless of the time.
*
* @see #setLocalNightMode(int)
*/
public static final int MODE_NIGHT_NO = 1;
/**
* Night mode which uses always uses a dark mode, enabling {@code night} qualified
* resources regardless of the time.
*
* @see #setLocalNightMode(int)
*/
public static final int MODE_NIGHT_YES = 2;
/**
* Night mode which uses a dark mode when the system's 'Battery Saver' feature is enabled,
* otherwise it uses a 'light mode'. This mode can help the device to decrease power usage,
* depending on the display technology in the device.
*
* <em>Please note: this mode should only be used when running on devices which do not
* provide a similar device-wide setting.</em>
*
* @see #setLocalNightMode(int)
*/
public static final int MODE_NIGHT_AUTO_BATTERY = 3;
切换主题的方法也很简单,直接调用以下方法即可。
AppCompatDelegate.setDefaultNightMode()
注意如果需要设置的主题会当前主题不一致时,会导致Activity的重建,即会主动调用recreate方法,这时候如果activity不适宜重新创建,如正在播放视频等,重新创建会打断用户操作,这用户体验是不好的。官方也提供了方法来解决这个问题,只需要在对应activity中增加uiMode的配置判断。
<activity android:name=".MainActivity"
android:configChanges="uiMode">
然后在onConfigurationChanged中处理自己的逻辑即可。
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
onLightModeChange();
}
至此深色模式的准备工作都做完了,来看下效果。



注意事项
- 代码中尽可能不要有硬编码的色值,除非确定了模式切换后的色值也保持不变。尽可能通过资源重定向,读取样式表中的内容。
- 设置后的模式值需要自己保存到本地。
- 动态获取Drawable Color时,需要通过Activity的上下文来获取,而不能通过Application来获取,因为Application获取到的是系统主题,如果当前系统属于暗色,则会获取到暗色的图片或色值。为了方便全局使用,这里可以通过使用Top Activity来获取
//获取Drawable
public static Drawable getDrawable(Activity activity) {
return ContextCompat.getDrawable(activity, drawableResId);
}
//获取Color
public static int getColor(Activity activity,@ColorRes int colorResId) {
return ContextCompat.getColor(activity, colorResId);
}
- Glide 使用填充图或错误图时,不能直接使用resource id,同样是因为主题的原因,但是通过设置主题也无法解决。Github Issue中也使用了一种折中方案,通过转成Drawable之后再来设置。 Glide无法加载暗夜资源
RequestOptions options = new RequestOptions()
.error(ResUtil.getDrawable(defIcon))
.placeholder(ResUtil.getDrawable(defIcon));
- 当应用的主题背景发生更改(无论是通过系统设置还是 AppCompat)时,会触发 [
uiMode配置变更。这意味着系统会自动重新创建 Activity。注意无论当前应用设置的属性是哪一种,只要系统设置触发的变更,都会导致uiMode的变更或Activity的重建,即使当前的主题与系统一致。所以如果是应用中有亮色或暗色的设置项时,应该避免系统设置而导致的Activity重建,这种使用体验是不好的。而且值得注意的是,如果当前主题和切换后的系统主题一致时,uiMode会触发一次变更,而如果不一致时,会触发两次变更(可以理解为两次触发重新设置为原来的模式) - Activity的重建在大部分手机上会闪黑屏,视觉效果是不好的,如果界面元素不多,可以考虑通过界面刷新来设置,即把当前的界面的UI重新设置一遍资源,这样UI的刷新就不会出现闪屏的问题,视觉效果是比较好的。
- 导航栏,状态栏及Toast等背景色,尽可能自己定义,因为不同厂商的区分可能会导致深色暗色的适配不一致。
至此,深色模式的适配也完成了,写下这篇博客用于记录适配过程踩到的坑,也希望能给有同样需求的小伙伴提供一点帮助,也欢迎各位大神指导交流。