在 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 適配。
妥妥的。