關注我的簡書個人部落格原文連結
一、簡介
鑒于Android Q 适配如火如荼的情況,我們今天也來講講Android Q全新的深色主題背景。不過該項功能,魅族已經推出兩年多了,隻不過名字我們叫夜間模式,也可見Google有點反借鑒國内廠商的意思。附上兩張效果圖:
原生深色模式效果:
魅族夜間模式效果
閑話到此為止,我們還是來說說Android Q深色模式的适配方式跟實作原理吧。
二、如何開啟
從文檔上我們可以可知,打開深色模式有三個方法:
- 設定 -> 顯示 -> 深色主題背景
- 下拉通知欄中開啟
- 原生安卓開啟省電模式時會自動激活深色模式
打開之後會發現,系統應用基礎都切換成深色模式,但第三方應用并沒有什麼效果,或是隻有部分界面變黑,這又是為何呢?
不要急,下面我們就來講講如何适配深色模式
三、适配方案
3.1.應用主題繼承 DayNight 主題
Android其實的Support包從23.2版本就開始支援夜間模式,也就是DayNight Mode。接下來我們就講講如何從uimode的角度去适配
- 繼承AppCompatActivity
- 使用Theme.AppCompat.DayNight這個Theme,例如如果你之前的應用Theme是:
Theme.AppCompat.NoActionBar
改為:
Theme.AppCompat.DayNight.NoActionBar
繼承後,如果目前開啟了夜間模式,系統會自動從 night-qualified 中加載資源,是以應用的顔色、圖示等資源應盡量避免寫死,而是推薦使用新增 attributes 指向不同的資源,如
?android:attr/textColorPrimary
?attr/colorControlNormal
附上一張Material Design經典的顔色設定指導圖:
如何自定義深色模式下的樣式
首先在res下,建立一個新的values-night目錄,然後在該目錄下建立一個styles.xml,把夜間模式的顔色在這裡設定下。
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
</style>
這樣在切換到深色模式時就會使用values-night的資源。你不用把整個styles.xml的内容都複制過來,例如你隻想改colorPrimary,你就設定一個colorPrimary就行了。
當然了,不隻是styles.xml,colors.xml,dimens.xml,strings.xml都可以這麼做。意思這些資源調用都是就近原則,切換到夜間模式時values-night有就使用values-night的,沒有還是使用values的。
應用内主動切換白天、深色模式
如果希望可以在應用内主動切換白天、深色模式,在Application中設定初始化一下Theme。 例如像這樣:
static {
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_YES);
}
然後改變Theme的方法就是
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
recreate(); // 這個是重新整理,不然不起作用
有四個模式可以選
MODE_NIGHT_NO // 日間模式,使用light theme
MODE_NIGHT_YES // 夜間模式,使用dark theme
MODE_NIGHT_AUTO // 根據系統時間自動切換
MODE_NIGHT_FOLLOW_SYSTEM // 跟随系統設定,這個是預設的
3.2、通過 forceDarkAllowed 啟用
如果嫌上面的方案麻煩,麻煩也有更簡單的方案,那就是forceDarkAllowed強制轉換,但是效果可能就沒有自己适配那麼完美了。
首先,可以通過在主題中添加 android:forceDarkAllowed=“true” 标記,這樣系統在夜間模式時,會強制改變應用顔色,自動進行适配。不過如果你的應用本身使用的就是 DayNight 或 Dark Theme,forceDarkAllowed 是不會生效的。
另外,如果你不希望某個 view 被強制夜間模式處理,則可以給 view 添加
android:forceDarkAllowed="false"
或者 view.setForceDarkAllowed(false),設定之後,即使打開了夜間模式且主題添加了 forceDarkAllowed,該 view 也不會變深色。比較重要的一點是,這個接口隻能關閉夜間模式,不能開啟夜間模式,也就是說,如果主題中沒有顯示聲明 forceDarkAllowed,
view.setForceDarkAllowed(true)
是沒辦法讓 view 單獨變深色的。如果 view 關閉了夜間模式,那麼它的子 view 也會強制關閉夜間模式
總結如下:
- 主題若添加 forceDarkAllowed=false,無論 view 是否開啟 forceDarkAllowed 都不會打開夜間模式
- 主題若添加 forceDarkAllowed=true,view 可以通過 forceDarkAllowed 關閉夜間模式,一旦關閉,子 view 的夜間模式也會被關閉
- 如果父 view 或主題設定了 forceDarkAllowed=false,子 view 無法通過 forceDarkAllowed=true 單獨打開夜間模式為
- 若使用的是 DayNight 或 Dark Theme 主題,則所有 forceDarkAllowed 都不生效
四、實作原理
上面我們說的 android:forceDarkAllowed 其實是分為兩個用處,它們分别的定義如下:
Activity Theme級别
//frameworks/base/core/res/res/values/attrs.xml
<declare-styleable name="Theme">
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>
View級别
//frameworks/base/core/res/res/values/attrs.xml
<declare-styleable name="View">
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>
4.1、Theme級别
熟悉View樹的構造原理的同學應該都知道,ViewRootImpl是View中的最高層級,屬于所有View的根,是以該級别,我們需要在ViewRootImpl中查找原因,尋尋覓覓,最終在updateForceDarkMode函數中找到關于forceDarkAllowed屬性的蹤影
//frameworks/base/core/java/android/view/ViewRootImpl.java
private void updateForceDarkMode() {
//渲染線程為空,直接傳回
if (mAttachInfo.mThreadedRenderer == null) return;
//判斷目前uimode是否開啟深色模式
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
if (useAutoDark) {
//讀取開發者選項中強制smart dark的值
boolean forceDarkAllowedDefault =
SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
//讀取Theme是淺色主題或深色主題,并且配置了forceDarkAllowed=true,
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);
}
}
updateForceDarkMode 調用的時機分别是在 ViewRootImpl#setView 和 ViewRootImpl#updateConfiguration,也就是DecorView初始化和uimode切換的時候調用,確定在設定中切換深色模式時,能通知ViewRootImpl進行界面重新整理。
我們繼續跟蹤 下HardwareRenderer#setForceDark 函數
//frameworks/base/graphics/java/android/graphics/HardwareRenderer.java
public boolean setForceDark(boolean enable) {
//當forceDark值發生變化才會進入下面邏輯,否則傳回false,無需重新整理界面
if (mForceDark != enable) {
mForceDark = enable;
nSetForceDark(mNativeProxy, enable);
return true;
}
return false;
}
最終發現,這是一個 native 方法,nSetForceDark的真正實作是在ThreadedRenderer.cpp中
//frameworks/base/core/jni/android_view_ThreadedRenderer.cpp
static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz,
jlong proxyPtr, jboolean enable) {
//将proxyPtr強轉成RenderProxy
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setForceDark(enable);
}
RenderProxy是一個代理類,也是MainThread和RenderThread通信的橋梁。關于MainThread、RenderThread的概念,後面會再單獨講述,這裡不作展開。
//frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
void RenderProxy::setForceDark(bool enable) {
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}
這裡從MainThread post了一個調用CanvasContext成員函數setForceDark的任務到RenderThread渲染線程
//frameworks/base/libs/hwui/renderthread/CanvasContext.h
void setForceDark(bool enable) {
mUseForceDark = enable;
}
bool useForceDark() {
return mUseForceDark;
}
發現隻是設定了一個mUseForceDark變量而已,并沒有看到關鍵性的調用。我們隻能繼續再跟一下mUseForceDark這個變量在哪裡使用到了。最終發現,是在TreeInfo中被指派給disableForceDark變量
//frameworks/base/libs/hwui/TreeInfo.cpp
TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext)
: mode(mode)
, prepareTextures(mode == MODE_FULL)
, canvasContext(canvasContext)
, damageGenerationId(canvasContext.getFrameNumber())
//初始化 TreeInfo 的 disableForceDark 變量,注意變量值意義的變化,0 代表打開夜間模式,>0 代表關閉夜間模式
, disableForceDark(canvasContext.useForceDark() ? 0 : 1)
, screenSize(canvasContext.getNextFrameSize()) {}
} // namespace android::uirenderer
而最終disableForceDark是在RenderNode中使用,調用路徑為:
prepareTree–>prepareTreeImpl–>pushStagingDisplayListChanges–>syncDisplayList–>handleForceDark
而最核心當屬handleForceDark函數:
//frameworks/base/libs/hwui/RenderNode.cpp
void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
// // 若沒打開強制夜間模式,直接退出
if (CC_LIKELY(!info || info->disableForceDark)) {
return;
}
auto usage = usageHint();
const auto& children = mDisplayList->mChildNodes;
//根據是否有文字、是否有子節點、子節點數量等情況,得出目前 Node 屬于 Foreground 還是 Background
if (mDisplayList->hasText()) {
usage = UsageHint::Foreground;
}
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);
}
}
//根據 UsageHint 設定變色政策:Dark(壓暗)、Light(提亮)
mDisplayList->mDisplayList.applyColorTransform(
usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
//frameworks/base/libs/hwui/RecordingCanvas.cpp
// frameworks/base/libs/hwui/RecordingCanvas.cpp
void DisplayListData::applyColorTransform(ColorTransform transform) {
// transform: Dark 或 Light
// color_transform_fns 是一個對應所有繪制指令的函數指針數組,主要是對 op 的 paint 變色或對 bitmap 添加 colorfilter
this->map(color_transform_fns, transform);
}
template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
auto end = fBytes.get() + fUsed;
// 周遊目前的繪制的 op
for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
auto op = (const Op*)ptr;
auto type = op->type;
auto skip = op->skip;
// 根據 type 找到對應的 fn,根據調用關系,我們知道 fns 數組對應 color_transform_fns,這個數組其實是一個函數指針數組,下面看看定義
if (auto fn = fns[type]) { // We replace no-op functions with nullptrs
// 執行
fn(op, args...); // to avoid the overhead of a pointless call.
}
ptr += skip;
}
}
#define X(T) colorTransformForOp<T>(),
static const color_transform_fn color_transform_fns[] = {
X(Flush)
X(Save)
X(Restore)
X(SaveLayer)
X(SaveBehind)
X(Concat)
X(SetMatrix)
X(Translate)
X(ClipPath)
X(ClipRect)
X(ClipRRect)
X(ClipRegion)
X(DrawPaint)
X(DrawBehind)
X(DrawPath)
X(DrawRect)
X(DrawRegion)
X(DrawOval)
X(DrawArc)
X(DrawRRect)
X(DrawDRRect)
X(DrawAnnotation)
X(DrawDrawable)
X(DrawPicture)
X(DrawImage)
X(DrawImageNine)
X(DrawImageRect)
X(DrawImageLattice)
X(DrawTextBlob)
X(DrawPatch)
X(DrawPoints)
X(DrawVertices)
X(DrawAtlas)
X(DrawShadowRec)
X(DrawVectorDrawable)
};
#undef X
color_transform_fn 宏定義展開
template <class T>
constexpr color_transform_fn colorTransformForOp() {
if
// op 變量中是否同時包含 paint 及 palette 屬性,若同時包含,則是繪制 Image 或者 VectorDrawable 的指令
// 參考:frameworks/base/libs/hwui/RecordingCanvas.cpp 中各 Op 的定義
constexpr(has_paint<T> && has_palette<T>) {
return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);
// 關鍵變色方法,根據 palette 疊加 colorfilter
transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette);
};
}
else if
// op 變量中是否包含 paint 屬性,普通繪制指令
constexpr(has_paint<T>) {
return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);
// 關鍵變色方法,對 paint 顔色進行變換
transformPaint(transform, const_cast<SkPaint*>(&(op->paint)));
};
}
else {
// op 變量不包含 paint 屬性,傳回空
return nullptr;
}
}
static const color_transform_fn color_transform_fns[] = {
// 根據 Flush、Save、DrawImage等不同繪制 op,傳回不同的函數指針
colorTransformForOp<Flush>
...
};
讓我們再一次看看 map 方法
template <typename Fn, typename... Args>
inline void DisplayListData::map(const Fn fns[], Args... args) const {
auto end = fBytes.get() + fUsed;
for (const uint8_t* ptr = fBytes.get(); ptr < end;) {
auto op = (const Op*)ptr;
auto type = op->type;
auto skip = op->skip;
if (auto fn = fns[type]) { // We replace no-op functions with nullptrs
// 對 op 的 paint 進行顔色變換或疊加 colorfilter
fn(op, args...); // to avoid the overhead of a pointless call.
}
ptr += skip;
}
}
我們先來整理下:
- CanvasContext.mUseForceDark 隻會影響 TreeInfo.disableForceDark 的初始化
- TreeInfo.disableForceDark 若大于 0,RenderNode 在執行 handleForceDark 就會直接退出
- handleForceDark 方法裡會根據 UsageHint 類型,對所有 op 中的 paint 顔色進行變換,如果是繪制圖檔,則疊加一個反轉的 colorfilter。變換政策有:Dark、Light
接下來讓我們來看 paint 和 colorfilter 的變色實作,這部分是交由CanvasTransform中處理:
bool transformPaint(ColorTransform transform, SkPaint* paint) {
applyColorTransform(transform, *paint);
return true;
}
static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
if (transform == ColorTransform::None) return;
//關鍵代碼,對顔色進行轉換
SkColor newColor = transformColor(transform, paint.getColor());
paint.setColor(newColor);
if (paint.getShader()) {
SkShader::GradientInfo info;
std::array<SkColor, 10> _colorStorage;
std::array<SkScalar, _colorStorage.size()> _offsetStorage;
info.fColorCount = _colorStorage.size();
info.fColors = _colorStorage.data();
info.fColorOffsets = _offsetStorage.data();
SkShader::GradientType type = paint.getShader()->asAGradient(&info);
if (info.fColorCount <= 10) {
switch (type) {
case SkShader::kLinear_GradientType:
for (int i = 0; i < info.fColorCount; i++) {
info.fColors[i] = transformColor(transform, info.fColors[i]);
}
paint.setShader(SkGradientShader::MakeLinear(info.fPoint, info.fColors,
info.fColorOffsets, info.fColorCount,
info.fTileMode, info.fGradientFlags, nullptr));
break;
default:break;
}
}
}
if (paint.getColorFilter()) {
SkBlendMode mode;
SkColor color;
// TODO: LRU this or something to avoid spamming new color mode filters
if (paint.getColorFilter()->asColorMode(&color, &mode)) {
color = transformColor(transform, color);
//将轉換後的顔色通過ColorFilter的方式設定到畫筆上
paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode));
}
}
}
接下來我們來看看顔色是如何進行轉換的:
static SkColor transformColor(ColorTransform transform, SkColor color) {
switch (transform) {
case ColorTransform::Light: //要求變亮
return makeLight(color);
case ColorTransform::Dark://要求變暗
return makeDark(color);
default:
return color;
}
}
//顔色提亮
static SkColor makeLight(SkColor color) {
//從顔色從rgb轉換成Lab模式
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;
}
}
//顔色變深
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;
}
}
可以很清楚的看到,深色模式的變色規則,就是從paint的color中提取出明度,然後根據目前是淺色模式還是深色模式,對明度進行相應的調整,以達到更好的顯示效果。
再來看看對圖檔的變換:
bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) {
// 根據 palette 和 colorfilter 判斷圖檔是亮還是暗的
palette = filterPalette(paint, palette);
bool shouldInvert = false;
if (palette == BitmapPalette::Light && transform == ColorTransform::Dark) {
// 圖檔本身是亮的,但是要求變暗,反轉
shouldInvert = true;
}
if (palette == BitmapPalette::Dark && transform == ColorTransform::Light) {
// 圖檔本身是暗的,但是要求變亮,反轉
shouldInvert = true;
}
if (shouldInvert) {
SkHighContrastConfig config;
config.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;
// 疊加一個亮度反轉的 colorfilter
paint->setColorFilter(SkHighContrastFilter::Make(config)->makeComposed(paint->refColorFilter()));
}
return shouldInvert;
}
到這裡Theme級别的forceDarkAllowed要講完了,你看明白了嗎?
4.2、View級别
View 級别的 forceDarkAllowed,通過 View 級别 forceDarkAllowed 可以關掉它及它的子 view 的夜間模式開關。因為是View級别,那入口很有可能就在構造方法中。事不宜遲,我們這就去看看是不是這樣的。
// frameworks/base/core/java/android/view/View.java
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
......
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
......
//果不其然,我們要找的就在這裡..
case R.styleable.View_forceDarkAllowed:
//這裡又回到前面我們分析的native層的RenderNode裡面
mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
break;
}
}
}
}
Java層的RenderNode的成員函數setForceDarkAllowed會調用另外一個成員函數nSetAllowForceDark,而nSetAllowForceDark是一個jni函數,由Native層的android_view_RenderNode_setAllowForceDark實作的:
#define SET_AND_DIRTY(prop, val, dirtyFlag) \
(reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().prop(val) \
? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \
: false)
static jboolean android_view_RenderNode_setAllowForceDark(jlong renderNodePtr, jboolean allow) {
return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC);
}
android_view_RenderNode_setAllowForceDark函數通過宏SET_AND_DIRTY來設定RenderNode的dark屬性。宏SET_AND_DIRTY首先是調用RenderNode的mutateStagingProperties獲得一個RenderProperties對象,如下所示:
//frameworks/base/libs/hwui/RenderNode.h
class RenderNode : public VirtualLightRefBase {
public:
......
RenderProperties& mutateStagingProperties() {
return mStagingProperties;
}
......
privagte:
......
RenderProperties mStagingProperties;
......
};
宏SET_AND_DIRTY接着再調用獲得的RenderProperties對象的成員函數setAllowForceDark設定一個Render Node的AllowForceDark屬性,如下所示:
//frameworks/base/libs/hwui/RenderProperties.h
class ANDROID_API RenderProperties {
......
bool setAllowForceDark(bool allow) {
return RP_SET(mPrimitiveFields.mAllowForceDark, allow);
}
bool getAllowForceDark() const {
return mPrimitiveFields.mAllowForceDark;
}
private:
// Rendering properties
struct PrimitiveFields {
bool mAllowForceDark = true;
...
} mPrimitiveFields;
RenderProperties對象的成員函數setAllowForceDark通過宏RP_SET來設定一個Render Node的AllowForceDark屬性,這個AllowForceDark屬性儲存在Render Node内部的一個RenderProperties對象的成員變量mPrimitiveFields描述的一個PrimitiveFields對象的成員變量mAllowForceDark中。
如果一個Render Node的allowForceDark屬性發生了變化,也就是它之前的allowForceDark值與新設定的allowForceDark值不一樣,那麼宏RP_SET的傳回值就為true。在這種情況下,宏SET_AND_DIRTY就會調用Render Node的成員函數setPropertyFieldsDirty标記它的屬性發生了變化,以便後面在渲染該Render Node的Display List時,可以進行相應的處理。
從前面分析的這個AllowForceDark屬性設定過程就可以知道,每一個View關聯的Render Node在内部通過一個RenderProperties對象儲存了它的一些屬性。當這些屬性發生變化時,不必重新建構View的Display List,而隻需要修改上述的RenderProperties對象相應成員變量值即可。通過這種方式,就可以提到應用程式視窗的渲染效率。
好了,回到話題 ,和 Theme 級别的一樣,這裡僅僅隻是設定到mProperties變量中而已,關鍵是要看哪裡使用這個變量,經過查找,我們發現,它的使用同樣在 RenderNode 的 prepareTreeImpl 中:
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {
...
// 1. 如果 view 關閉了夜間模式,會在這裡讓 info.disableForceDark 加 1
// 2. info.disableForceDark 正是 handleForceDark 中關鍵變量,還記得嗎?
// 3. nfo.disableForceDark 大于 0 會讓此 RenderNode 跳過夜間模式處理
// 4. 如果 info.disableForceDark 本身已經大于 0 了,view.setForceDarkAllowed(true) 也毫無意義
if (!mProperties.getAllowForceDark()) {
info.disableForceDark++;
}
prepareLayer(info, animatorDirtyMask);
if (info.mode == TreeInfo::MODE_FULL) {
// 這裡面會調用 handleForceDark 方法處理夜間模式
pushStagingDisplayListChanges(observer, info);
}
if (mDisplayList) {
info.out.hasFunctors |= mDisplayList->hasFunctor();
// 遞歸調用子 Node 的 prepareTreeImpl 方法
bool isDirty = mDisplayList->prepareListAndChildren(
observer, info, childFunctorsNeedLayer,
[](RenderNode* child, TreeObserver& observer, TreeInfo& info,
bool functorsNeedLayer) {
child->prepareTreeImpl(observer, info, functorsNeedLayer);
});
if (isDirty) {
damageSelf(info);
}
}
...
// 重要,把 info.disableForceDark 恢複回原來的值,不讓它影響 Tree 中同級的其他 RenderNode
// 但是本 RenderNode 的子節點還是會受影響的,這就是為什麼父 view 關閉了夜間模式,子 view 也會受影響的原因
// 因為還原 info.disableForceDark 操作是在周遊子節點之後執行的
if (!mProperties.getAllowForceDark()) {
info.disableForceDark--;
}
...
}
五、總結
Android Q深色模式原理流程并不複雜,主要是對顔色進行了處理和設定了colorFilter,這個跟魅族的夜間模式有着異曲同工之妙,隻不過一個是 Java層,一個是 Native層。而這套邏輯難的部分就在DisplayList、RenderNode 等圖形相關的概念,這部分後續有機會再作展開。
這裡也順道說一下Android Q這套深色模式的優缺點,能力有限,歡迎指正。
優點
- 适配簡單,容易上手
- 借助于硬體加速,渲染性能比較高效
- 在native層做顔色處理,比java效率更高
缺點
- 修改難度較大,需要對RenderNode/DrawOp/DisplayList等知識較為熟悉
- 在軟體繪制界面無法産生效果
- 同樣不支援WebView