在 Android 10 里,Dark theme 暗黑模式获得了系统级的支持。 暗黑模式不只酷炫,并且有下降屏幕耗电、在光线较暗的环境中使用更温馨等好处。 今天带你们看一下如何适配暗黑模式,本文会从如下几点进行介绍:html
相信本文会让你对暗黑模式有一个更全面的了解。java
在 Android 10 系统设置里增长了暗黑模式的开关,但除了系统设置,咱们也能够本身动态开启。 假如咱们项目里面有一个按钮用来开关暗黑模式,能够这样作:android
btn.setOnClickListener {
if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
// 关闭暗黑模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
} else {
// 开启暗黑模式
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
}
}
复制代码
若是当前开启了暗黑模式就关掉,反之开启。 你可能还看过另外一种 delegate.localNightMode 的写法,一样也是能够生效的,它们的区别在于做用范围不一样:安全
// 做用于当前项目的全部组件
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
// 只做用于当前组件
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
复制代码
另外须要注意的是,在默认状况下,设置暗黑模式会重走 Activity 生命周期,须要从新渲染整个页面,因此不要在 onCreate 里直接设置。 若是不想重走生命周期,能够给 Activity 配置 android:configChanges="uiMode",但这样一来就须要在 onConfigurationChanged() 方法里进行手动适配。app
上面用到了 YES 和 NO 两种暗黑的状态,但其实还不止这两种,暗黑模式一共有这几种状态:函数
因为不少定制系统对省电模式进行了魔改,因此使用 MODE_NIGHT_AUTO_BATTERY 不必定会生效。 另外,当 DefaultNightMode 和 LocalNightMode 都是默认值 MODE_NIGHT_UNSPECIFIED 的时候,会做 MODE_NIGHT_FOLLOW_SYSTEM 跟随系统处理。布局
下面要开始对暗黑模式进行适配啦。咱们使用 Android Studio 的 Basic Activity 模板建立一个项目,对它进行暗黑模式适配的改造。字体
第一步,找到当前项目使用的主题,将默认使用的 Theme.AppCompat.Light 主题修改成 Theme.AppCompat.DayNight:ui
<style name="AppTheme" parent="Theme.AppCompat.DayNight"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style>
复制代码
第二步,没有第二步了,如今这个项目已经支持暗黑模式了,开启暗黑模式就能看到效果:this
是否是很简单,但直觉告诉咱们确定没有这么简单。
咱们进入 MainActivity 的布局文件 activity_main,能够发现这里面是彻底没有使用硬编码的。 什么叫硬编码?就是咱们平时所说的「写死」。要是咱们写死了一个色值,暗黑模式还能生效吗? 立刻试一下,咱们给根布局写死一个白色背景 android:background="#FFFFFF",切换暗黑模式就变成了这样:
能够看到,在写死色值的状况下暗黑模式就失效了。下面看看对于自定义的色值,要如何适配。
在 colors.xml 里添加一个配置颜色,好比:
<color name="color_bg">#FFFFFF</color>
复制代码
这个是在普通模式下使用的色值,为了适配暗黑模式,还须要一个在暗黑模式下对应的色值。 新建 values-night 目录,并把对应色值配置到这个目录下的 colors.xml 文件。
将根布局的背景颜色修改成 color_bg,这样就能使用咱们本身想要的颜色进行适配了:
在暗黑模式下,系统会优先从 night 后缀的目录下找到对应的资源配置。 以上就是使用 DayNight 主题进行暗黑模式适配的所有内容了。
一些关于 Android 10 暗黑模式适配的文章到这里就结束了,但其实 DayNight 主题并非 Android 10 新增的东西,它早在 Android 6.0 就已经出现。虽然它涉及的内容很少,但你们可能也发现了,在实际项目中它的可操做性并不高。 首先,使用这种适配方式,要求咱们整个项目全部的色值都不能使用硬编码,要作到这一点已经很不容易了,不少项目连统一的设计规范都很难作到。再退一步讲,就算咱们全部色值都是使用 xml 配置的,但 colors.xml 里配置了成百上千个色值,咱们须要对全部这些色值配置一个对应的暗黑色值,而且要确保它们在暗黑模式下能比较美观的展现。 因此,除非项目自己已经有一套严格的设计规范而且严格执行了,不然使用 DayNight 主题适配暗黑模式基本是不具备可操做性的。 Android 10 新增的固然不仅是一个暗黑模式的开关而已,下面咱们看一下 Android 10 有什么新特性供咱们适配。
其实咱们的需求很明确,就是使用了硬编码也能被适配成暗黑模式。Android 10 新增的 Force Dark 强制暗黑就实现了这个功能。
仍是回到刚才的项目,把背景写死白色,再次来到 styles.xml 的主题配置。此次咱们不用 DayNight 主题了,把配置改为以下:
<style name="AppTheme" parent="Theme.AppCompat.Light"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="android:forceDarkAllowed">true</item> </style>
复制代码
咱们把主题换回 Light 亮色主题,至于为何要用 Light 后面源码部分还会再讲到 另外,重点来了,这里还增长了一个 forceDarkAllowed 的配置,这是 compileSdkVersion 升级到 29 新增的配置,按字面意思就是「开启强制暗黑」。 这样就已经完成配置了,在 Android 10 的机器上运行一下,切换暗黑模式,记住此次的背景是写死白色的:
背景被强制转换成黑色了,细心的还会发现,右下角按钮的背景颜色也变深了。 Force Dark 这么暴力,连咱们写死的色值都改了,虽然方便,但这也给咱们一种不安全感。 要是 Force Dark 适配出来的颜色不是咱们想要的怎么办?咱们还能自定义暗黑色值吗?也是能够的。
除了主题新增了 forceDarkAllowed 这个配置,View 里面也有。 若是某个 View 的须要使用自定义色值适配暗黑模式,咱们须要对这个 View 添加这个配置,让 Force Dark 排除它:
android:forceDarkAllowed="false"
复制代码
而后在代码里根据当前是否处于暗黑模式,对色值进行动态设置。 对于 View 的 forceDarkAllowed,有几点须要注意:
综上能够看出,其实目前并无很好的 Force Dark 自定义方案。好在 Force Dark 的总体效果没什么大问题,就算要自定义,咱们也尽可能只对子 View 进行自定义。
下面咱们看一下源码,看看系统在暗黑模式下是如何对颜色进行转换的。 这里仅展现几个关键源码片断,它们之间是如何调用的就不赘述啦。
看源码首先咱们要找到入口,入口就是主题的 forceDarkAllowed 配置,搜索一下能够发现这个配置会在 ViewRootImpl 被用到。 相关的说明已经用注释写在代码里了。
// android.view.ViewRootImpl.java
private void updateForceDarkMode() {
if (mAttachInfo.mThreadedRenderer == null) return;
// 判断当前是否处于暗黑模式
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
if (useAutoDark) {
// 这个是被用来做为默认值用的,这里先无论它,咱们后面还会讲到。
boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
// 判断当前是否为 Light 主题,这也是为何咱们前面要使用 Light 主题。这也很好理解,只有当前主题是亮色的时候,才须要进行暗黑的处理。
// 判断当前是否容许开启强制暗黑,咱们就是靠它找到这个地方的。
useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
&& a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
a.recycle();
}
if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {
// TODO: Don't require regenerating all display lists to apply this setting
invalidateWorld(mView);
}
}
复制代码
总结一下,根据这个方法咱们能够知道,Force Dark 生效有三个条件:
源码再跟下去,发现调用了 Native 代码。
下一个关键代码是 RenderNode 的 handleForceDark 函数。RenderNode 是绘制节点,一个 View 能够有多个绘制节点,好比一个 TextView 的文字部分是一个绘制节点,它设置的背景也是一个绘制节点。看一下这个函数作了什么。
// frameworks/base/libs/hwui/RenderNode.cpp
void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
if (CC_LIKELY(!info || info->disableForceDark)) {
return;
}
// 这个函数看似有点复杂,但其实咱们只须要关注 usage 这个参数。
// usage 有两个取值,Foreground 前景和 Background 背景。
auto usage = usageHint();
const auto& children = mDisplayList->mChildNodes;
if (mDisplayList->hasText()) {
// 若是当前节点 hasText() 含有文字,那它就是一个 Foreground 前景
usage = UsageHint::Foreground;
}
// 下面的判断都是设为 Background 背景
if (usage == UsageHint::Unknown) {
if (children.size() > 1) {
usage = UsageHint::Background;
} else if (children.size() == 1 &&
children.front().getRenderNode()->usageHint() !=
UsageHint::Background) {
usage = UsageHint::Background;
}
}
if (children.size() > 1) {
// Crude overlap check
SkRect drawn = SkRect::MakeEmpty();
for (auto iter = children.rbegin(); iter != children.rend(); ++iter) {
const auto& child = iter->getRenderNode();
// We use stagingProperties here because we haven't yet sync'd the children
SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(),
child->stagingProperties().getWidth(), child->stagingProperties().getHeight());
if (bounds.contains(drawn)) {
// This contains everything drawn after it, so make it a background
child->setUsageHint(UsageHint::Background);
}
drawn.join(bounds);
}
}
// 根据分类,若是是背景会被设为 Dark 深色,不然是 Light 亮色。
mDisplayList->mDisplayList.applyColorTransform(
usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
复制代码
这个函数作的就是对当前绘制节点进行 Foreground 仍是 Background 的分类。 为了保证文字的可视度,须要保证必定的对比度,在背景切换成深色的状况下,须要把文字部分切换成亮色。
根据分好的颜色类型,会进入 CanvasTransform 对颜色进行转换处理。这里也是 Force Dark 最核心的地方了。
// frameworks/base/libs/hwui/CanvasTransform.cpp
static SkColor transformColor(ColorTransform transform, SkColor color) {
switch (transform) {
case ColorTransform::Light:
// 转换为亮色
return makeLight(color);
case ColorTransform::Dark:
// 转换为暗色
return makeDark(color);
default:
return color;
}
}
复制代码
根据类型调用了对应的函数转换颜色,咱们看一下 makeDark 吧。
static SkColor makeDark(SkColor color) {
Lab lab = sRGBToLab(color);
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL < lab.L) {
lab.L = invertedL;
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}
复制代码
这里把 RGB 色值转换成了 Lab 的格式。 Lab 格式含有 L、a、b 三个参数,ab 对应色彩学上的两个维度,不用管它,咱们要关注的是里面的 L。 L 就是亮度,它的取值范围是 0 - 100,数值越小颜色就越暗,反之就越亮。这篇文章封面的安卓机器人右边颜色就是下降亮度后的效果。 回到代码来,这里用 110 减去当前亮度,能够说是对亮度作了取反。至于为何是用 110 而不是用 100,我猜想是为了不使用纯黑色。 在官方暗黑模式设计规范能够看到,建议使用深灰色做为背景,而不是用纯黑色。
最后比对取反的色值和原色值的亮度,将较暗的那个色值返回。 makeLight 函数也是相似的。
static SkColor makeLight(SkColor color) {
Lab lab = sRGBToLab(color);
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL > lab.L) {
lab.L = invertedL;
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}
复制代码
因此到这里咱们发现,其实 Force Dark 强制暗黑转换颜色的规则,或者说是它的本质,就是亮度取反。
若是你的项目 compileSdkVersion 已经升级到 29,那如今就能够开启 Force Dark 适配暗黑模式了。但不少项目要升级到 29 还有一段路要走,咱们有没有办法提早适配呢?
回到咱们开始看源码的地方:
boolean forceDarkAllowedDefault = SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
&& a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
复制代码
当取不到 Theme_forceDarkAllowed 的时候,会取 DEBUG_FORCE_DARK 做为默认值,在哪里能够开启这个 DEBUG_FORCE_DARK 呢? 在 Android 10 的开发者选项里面,能够发现多了一个这样的选项:
这里的「强制启用 SmartDark 功能」就是 DEBUG_FORCE_DARK 的开关,虽然咱们看了源码都知道它也没有多智能。 开启后会对全部项目生效,这样就能够提早用 Force Dark 进行适配了。
开启 Force Dark 后大几率会发现一些有问题的图片资源,好比带有固定背景的 icon 等。 若是项目有适配暗黑模式的计划,我的建议能够按如下几步走:
使用 DayNight 主题能够实现暗黑模式的适配,但这种方法在实际项目中可操做性不高。 Android 10 新增的暗黑模式特性叫 Force Dark 强制暗黑,只需给主题添加一个容许开启的配置便可。 Force Dark 的实现方式是下降背景亮度,提升字体亮度,本质是对色值进行亮度取反。 最后,在 Android 10 的设备上,能够开启开发者选项中的「强制启用 SmartDark」,提早用 Force Dark 适配。
妥妥的。