天天看點

Android Q深色模式源碼解析

關注我的簡書個人部落格原文連結

一、簡介

鑒于Android Q 适配如火如荼的情況,我們今天也來講講Android Q全新的深色主題背景。不過該項功能,魅族已經推出兩年多了,隻不過名字我們叫夜間模式,也可見Google有點反借鑒國内廠商的意思。附上兩張效果圖:

原生深色模式效果:

Android Q深色模式源碼解析

魅族夜間模式效果

Android Q深色模式源碼解析

閑話到此為止,我們還是來說說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經典的顔色設定指導圖:

Android Q深色模式源碼解析

如何自定義深色模式下的樣式

首先在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就行了。

Android Q深色模式源碼解析

當然了,不隻是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

繼續閱讀