Android Q深色模式源码解析

一、简介

鉴于Android Q 适配如火如荼的情况,我们今天也来讲讲Android Q全新的深色主题背景。不过该项功能,魅族已经推出两年多了,只不过名字我们叫夜间模式,也可见Google有点反借鉴国内厂商的意思。附上两张效果图:

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经典的颜色设置指导图:


image.png

如何自定义深色模式下的样式

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

image.png

当然了,不只是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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270