深色模式适配
目录
深色模式适配
深色模式简介
深色模式是 Google 在 Android 10
(Android Q
、API 29
) 版本中提供的核心功能。
实际上,在之前的 Android 9
(Android P
、API 28
) 系统中,就已经在设置项中提供了一个「深色主题」,不过这个主题并非全局,而是仅适用于下拉通知栏的开关面板和文件夹等少部分界面。
而从 Android Q 开始,Google 开始提供系统级的黑暗模式
,大部分预装应用、抽屉、设置菜单和 Google Feed 资讯流等界面和按钮,都会变成以黑色为主色调。
因此,我们只需要适配
Android 9(含)
及以后的系统版本即可。
几种适配方案
常规方案一:不重启模式 + 手动适配
给需要适配的 Activity 添加android:configChanges="uiMode"
- 切换深色模式不会导致 Activity 重启,而是会回调
onConfigurationChanged()
方法 - 系统只是
通知
你显示模式改变了,所有的适配工作都需要自行处理 - 如果不做任何处理,就等价于不适配深色模式(显示模式切换后 Activity 不会有任何改变)
- 不建议采用此方案:
- 优点:不会导致 Activity 重启,甚至可以自定义过渡动效,体验较好
- 缺点:
适配工作量较大
,所有 UI 的状态都需要手动一个一个的刷新- 需要处理各种奇奇怪怪的缓存、渲染、布局、事件分发等问题
- 也不是 Google 建议采用的方案
常规方案二:重启模式 + 自动适配【建议】
不做任何设置时的默认
效果(即没有添加android:configChanges="uiMode"
)
- 切换深色模式会导致 Activity
重启
- 仅当前显示的页面(栈顶 Activity)会重启,会重走
onCreate
方法 - 未显示的 Activity 会在推到栈顶时重启(而不是立即重启)
- Activity 重启时可能会保留一些状态信息(例如横竖屏状态)
- 进程不会重启
- 仅当前显示的页面(栈顶 Activity)会重启,会重走
- 切换深色模式后会自动使用
values-night
、drawable-night
等深色模式专用目录下的资源 - 建议采用此方案
- 优点:不会导致状态异常,不会出现各种奇奇怪怪的缓存、渲染、布局、事件分发等问题
- 缺点:当前页面会回到启动时的初始状态,导致信息丢失,体验不好
- 浏览的位置会重置:当前屏幕内可见的应用会被划走
- 接口可能会重新拉取:接口返回的内容也可能会改变
- 输入框中的内容会清空、弹窗会消失、会占用引导提示次数等
其他方案三:使用 Force Dark 功能一键适配
Android 10 中也提供了 Force Dark
功能,可让开发者快速实现深色主题背景。
- 使用条件:
- 安卓
10.0
及以上版本 - 应用的主题中包含
<item name="android:forceDarkAllowed">true</item>
属性- 或者在
onCreate
中、setContentView
之前,调用getWindow().getDecorView().setForceDarkAllowed(true);
- 此属性只有在
API 29
及以上才有,只能放在values-v29
目录内 - 要求
compileSdkVersion
必须在29
及以上,否则编译失败
- 或者在
- 父主题中需要有
<item name="android:isLightTheme">true</item>
属性- 一般只有带
.Light
的浅色主题中才有此属性,比如Theme.Light
、Theme.AppCompat.Light
、Theme.Material.Light
- 对于继承自
.DayNight
的主题,系统不会应用 Force Dark,因为这个主题本身有自己的适配方案(详见方案四) - 对于深色主题(例如
Dark
、Theme.Material
),系统也不会应用 Force Dark
- 一般只有带
- 安卓
- 原理:系统会分析浅色主题应用下的每一层 View,并且在这些 View 绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色
- 不建议采用此方案
- 优点:代码量少,自动替换
- 缺点:
不精致
:是一种简单粗暴的转换方式,转换效果通常是不尽如人意的- 存在很多bug:网上有很多反馈颜色反色异常的bug
- 有局限性:不兼容
API-29
之前的系统,只能使用浅色主题,WebView 支持不完善
也可以混合使用 Force Dark 和前两种方案,也即默认采用 Force Dark,在需要精致适配的场景使用方案一或方案二对效果进行微调。
其他方案四:继承 DayNight 主题一键适配
这种方案和 Force Dark
功能类似,也是在切换到深色模式
时让系统自动帮你适配文字颜色、背景色,个人感觉整体效果还不如 Force Dark
。
- 使用条件:继承自
Theme.AppCompat.DayNight
或Theme.MaterialComponents.DayNight
- 继承后,如果当前开启了深色主题,系统会自动帮你适配很多文字颜色、背景色内容,当然你可以通过指定
night-qualified
中的资源来人工干涉
其实这不能算是一种方案,因为这和前几个方案没啥特殊的地方,可以算是一个杂糅体。
其他方案五:很多阅读类用应用的自研方案
- 比如很多阅读类用应用都有一个
换肤
的功能- 原理:简单来讲,就是在换肤(切换主题)的时候,把原来的 Context 和 Resource 换掉,切换到对应主题的资源,每一个主题下都有同名的资源文件,在主题中没有找到对应 id 的资源文件,就取默认的
- 由于需要适配图片主题等等很多功能,所以实现这个功能要复杂一些
- 原本的换肤功能和深色模式是没有关系的
- 先有换肤后有深色模式
- 后来这些阅读类用应用为了适配深色模式,所以就在监听到深色模式切换时
切换到深色皮肤
,让你误以为是按照 Google 规范适配了深色模式,实际上并不是 - 此方案由于是完全自定义的,所以可以实现很多系统深色模式无法实现的功能,在和产品沟通时要明确:别人能实现的,我们不一定能实现,因为方案完全不一样
- 此方案改动太大,不适合普通应用,不建议采用
其他方案N:开源的 MultipleTheme 等方案
夜间模式的 Android 框架,配合 theme 和换肤控件框架可以做到无缝切换换肤(无需重启应用和当前页面)。
该应用框架可以实现无缝换肤/切换夜间模式的需求,需要在换肤/切换夜间模式的界面只需要使用框架里的自封装控件,其他界面的控件使用原生android控件即可。
核心功能
判断当前是否处于深色模式
public static boolean isNightMode(@NonNull Context context) {
int uiMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return uiMode == Configuration.UI_MODE_NIGHT_YES;
}
非 Activity 也可以通过如下方式判断状态
同一个进程内的不同 Context 获取的值可能是不一样的
手动切换深色模式
public static void switchNightMode(@NonNull Context context) {
//进行正常模式、暗黑模式切换
int uiMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
int nightMode = uiMode == Configuration.UI_MODE_NIGHT_NO ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO;
AppCompatDelegate.setDefaultNightMode(nightMode); //AppCompatActivity 中也可使用 getDelegate().setLocalNightMode(nightMode);
}
注意:Activity 必须继承自 AppCompatActivity
才能正常切换
- 如果 Activity 继承自
AppCompatActivity
- 【核心】必须给其设置一个
Theme.AppCompat
(或其后代)的主题- 例如:
android:theme="@style/Theme.AppCompat"
、Theme.AppCompat.Light"
- 否则,运行时崩溃:IllegalStateException: You need to use a
Theme.AppCompat
theme (or descendant) with this activity
- 例如:
- 手动切换模式后
- 仍会导致重启模式下 Activity 重启,不重启模式下触发
onConfigurationChanged
- 当前应用所有 Activity 均会受影响,但不影响其他应用,也不会改变系统设置中模式开关的状态
- 手动切换模式后,当前应用仍会正常响应系统设置中的模式切换,但也仅仅是事件响应而已
- 仍会导致不重启模式下触发
onConfigurationChanged
,当然也可以手动更新 UI - 【核心】仍会导致重启模式下 Activity 重启,但重启后仍然保持原先手动设置的模式,而不会匹配系统设置中的模式
- Force Dark 模式、
.DayNight
等主题也不会在匹配系统设置中的模式 - 总之一句话:手动设置的优先级 > 系统设置的优先级
- 仍会导致不重启模式下触发
- 仍会导致重启模式下 Activity 重启,不重启模式下触发
- 【核心】必须给其设置一个
- 如果 Activity 不继承自
AppCompatActivity
- 使用当前 Context 调用
setDefaultNightMode
方法,对自身及对其他 Activity 都无任何效果 - 在其他 Activity 中调用
setDefaultNightMode
方法切换模式后,对自身也无任何效果
- 使用当前 Context 调用
命令行切换深色模式
adb shell cmd uimode night yes(开)
adb shell cmd uimode night no(关)
adb shell cmd uimode night custom(定时)
adb shell cmd uimode night auto(日出日落)
监听深色模式状态改变
onConfigurationChanged
Activity 可以通过android:configChanges="uiMode"
监听uiMode
改变,在onConfigurationChanged()
中处理状态改变
此种监听方式会导致 Activity 不会重启,需要自行确保(手动处理)切换深色模式后页面反色是否正常
所有 View、Fragment 也都可以监听onConfigurationChanged
通过监听深色模式字段改变
Uri uri = Settings.System.getUriFor("xxxx"); //不同 ROM 下的 Uri 是不一样的
getContentResolver().registerContentObserver(uri, true, new ContentObserver(new Handler()) {
@Override
public boolean deliverSelfNotifications() {
return super.deliverSelfNotifications();
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
}
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
}
});
资源适配
可以将深色模式资源放置在values-night
、drawable-night
、drawable-night-xxx
中,当深色模式切换时,会使用对应深色模式的资源。
assets 目录中的资源不能采用这种方式
给图片指定透明度
可以通过指定透明度 alpha
来实现对深色模式的适配
android:alpha="@dimen/pic_alpha_dimen"
- 透明度
alpha
不仅可以用在图片上,也可以用在任意 View 上 - 在父 View 上加
alpha
和在子 View 上加alpha
的效果没有什么区别 - 父布局和子 View 上都加
alpha
时,效果会叠加
在代码中可以通过以下三种方案设置透明度:
<resources>
<integer name="pic_alpha_integer">204</integer>
<item name="pic_alpha_fraction" type="fraction">80%</item>
<item name="pic_alpha_dimen" format="float" type="dimen">0.8</item>
</resources>
//float:范围为 0.0- 1.0
float alpha = getResources().getFraction(R.fraction.pic_alpha_fraction, 1, 1);
imageView.setAlpha(alpha);
//int:范围为 0- 255
float alpha = getResources().getFraction(R.fraction.pic_alpha_fraction, 255, 1);
imageView.setAlpha((int) alpha); //必须强转为 int,否则调用的是另一个重载方法
imageView.setAlpha(getResources().getInteger(R.integer.pic_alpha_integer));
给图片加一个蒙层
public static void setImageViewColorFilter(@NonNull ImageView imageView) {
int color = imageView.getContext().getResources().getColor(R.color.night_color_tint);
imageView.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER));
}
颜色 night_color_tint 在浅色模式下是全透明颜色,在深色模式下是你需要的蒙层颜色
给 Drawable 设置透明度
public static void setTextViewDrawableAlpha(@NonNull TextView textView) {
Drawable[] drawables = textView.getCompoundDrawables();
for (Drawable drawable : drawables) {
if (drawable != null) {
drawable.setAlpha(0.8*255); //给 Drawable 设置透明度
}
}
}
Vector 图片适配
可以通过指定画笔颜色来实现对深色模式的适配
android:tint="@color/clear_red"
Lottie 动画适配
对于 Lottie 动画,我们可以使用其Dynamic Properties
特性来针对深色模式进行颜色变化。
- 调用
LottieAnimationView.resolveKeyPath()
方法获取动画的路径 - 定义深色模式和明亮模式的色值,在深色模式切换时,Lottie 动画的颜色会随着深色模式切换而变化
- 同样的对于播放动画,我们也可以设置描边颜色,来达到深色模式切换的效果
可能存在的问题
2021-03-07
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/14495880.html