惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 最新话题
Recorded Future
Recorded Future
月光博客
月光博客
博客园 - 【当耐特】
博客园 - 叶小钗
宝玉的分享
宝玉的分享
量子位
雷峰网
雷峰网
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog
Vercel News
Vercel News
L
LangChain Blog
B
Blog
Y
Y Combinator Blog
爱范儿
爱范儿
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
The Blog of Author Tim Ferriss
A
About on SuperTechFans
博客园 - Franky
P
Palo Alto Networks Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
云风的 BLOG
云风的 BLOG
C
Cisco Blogs
Scott Helme
Scott Helme
I
Intezer
T
The Exploit Database - CXSecurity.com
MyScale Blog
MyScale Blog
T
Tor Project blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Troy Hunt's Blog
N
News and Events Feed by Topic
大猫的无限游戏
大猫的无限游戏
F
Fortinet All Blogs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
S
Security Affairs
Cyberwarzone
Cyberwarzone
PCI Perspectives
PCI Perspectives
小众软件
小众软件
D
DataBreaches.Net
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
Forbes - Security
Forbes - Security
S
Securelist
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
The Last Watchdog
The Last Watchdog

chenhang 的博客

Android 12 - WMS 层级结构 && DisplayAreaGroup 引入 Android 12 - 跟踪利器 WinScope Android 12 - Letterbox 模式 Android 列表滚动优化之 OverScroller 揭秘 使用 Ninja 提升模块编译速度 如何顺滑地查看 Android Native 代码 AOSP 编译和烧写 Protocol Buffers 手册 设计模式之装饰模式 设计模式之桥接模式 设计模式之代理模式 依赖注入利器 - Dagger ‡ Robolectric使用教程 设计模式之模板方法模式和策略模式 设计模式之工厂模式(Factory) Fragment源码分析 Fragment事务管理源码分析 java动态代理 Android分包MultiDex源码分析
Android Q 黑暗模式(Dark Mode)源码解析
2019-10-22 · via chenhang 的博客

1. 简介

随着 Android Q 发布,「黑暗模式」或者说是「夜间模式」终于在此版本中得到了支持,官方介绍见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme,再看看效果图:

2019-10-21-17-21-50.png

其实这个功能魅族在两年前就已支持,不得不说 Android 有点落后了,今天我们就来看看原生是怎么实现全局夜间模的吧

2. 打开与关闭

从文档上我们可以可知,打开夜间模式有三个方法:

  • 设置 -> 显示 -> 深色主题背景
  • 下拉通知栏中开启
  • Pixel 手机开启省点模式时会自动激活夜间模式

3. 如何适配

打开后,我们会发现,除原生几个应用生效外,其他应用依然没有变成深色主题,那么应用该如何适配呢?官方提供了下面两种方法:

3.1. 让应用主题继承 DayNight 主题

1
<style name="AppTheme" parent="Theme.AppCompat.DayNight">

或者继承自

1
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">

继承后,如果当前开启了夜间模式,系统会自动从 night-qualified 中加载资源,所以应用的颜色、图标等资源应尽量避免硬编码,而是推荐使用新增 attributes 指向不同的资源,如

1
2
?android:attr/textColorPrimary
?attr/colorControlNormal

另外,如果应用希望主动切换夜间/日间模式,可以通过 AppCompatDelegate.setDefaultNightMode() 接口主动切换

3.2. 通过 forceDarkAllowed 启用

如果应用不想自己去适配各种颜色,图标等,可以通过在主题中添加 android:forceDarkAllowed="true" 标记,这样系统在夜间模式时,会强制改变应用颜色,自动进行适配(这个功能也是本文主要探讨的)。不过如果你的应用本身使用的就是 DayNightDark 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 单独打开夜间模式为
  • 若使用的是 DayNightDark Theme 主题,则所有 forceDarkAllowed 都不生效

4. 实现原理

通过继承主题适配夜间模式的原理本质是根据 ui mode 加载 night-qualified 下是资源,这个并非 Android Q 新增的东西,我们这里不再描述。现在主要来看看 forceDarkAllowed 是如何让系统变深色的。

既然一切的源头都是 android:forceDarkAllowed 这个属性,那我们就从它入手吧,首先我们要知道,上面我们说的 android:forceDarkAllowed 其实是分为两个用处,它们分别的定义如下:

frameworks/base/core/res/res/values/attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<declare-styleable name="View">






<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>

<declare-styleable name="Theme">







<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>

一个是 View 级别的,一个是 Theme 级别的。

4.1. Theme 级别 forceDarkAllowed

从上面的总结来看,Theme 级别的开关优先级是最高的,控制粒度也最大,我们看看源码里面使用它的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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);

useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
&& a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
a.recycle();
}


if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {

invalidateWorld(mView);
}
}


public boolean setForceDark(boolean enable) {
if (mForceDark != enable) {
mForceDark = enable;

nSetForceDark(mNativeProxy, enable);
return true;
}
return false;
}

这段代码还是比较简单,判断系统:

  • 是否打开了夜间模式
  • 是否使用浅色主题
  • Theme_forceDarkAllowed 是否为 true

三者同时为 true 时才会设置夜间模式,而 updateForceDarkMode 调用的时机分别是在 ViewRootImpl#setViewViewRootImpl#updateConfiguration,也就是初始化和夜间模式切换的时候都会调用,确保夜间模式能及时启用和关闭。继续跟踪 HardwareRenderer#setForceDark 发现,这是一个 native 方法,所以接下来让我们进入 native 世界,nSetForceDark 对应的实现位于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz,
jlong proxyPtr, jboolean enable) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setForceDark(enable);
}


void RenderProxy::setForceDark(bool enable) {
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}


class CanvasContext : public IFrameCallback {
public:

...

void setForceDark(bool enable) { mUseForceDark = enable; }

bool useForceDark() {
return mUseForceDark;
}

...

private:

...

bool mUseForceDark = false;

...
};

最终就是设置了一个 CanvasContext 的变量值而已,什么都还没有做,那么这个变量值的作用是什么,什么时候生效呢?我们进一步查看使用的地方:

1
2
3
4
5
6
7
8
9
10
11

TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext)
: mode(mode)
, prepareTextures(mode == MODE_FULL)
, canvasContext(canvasContext)
, damageGenerationId(canvasContext.getFrameNumber())

, disableForceDark(canvasContext.useForceDark() ? 0 : 1)
, screenSize(canvasContext.getNextFrameSize()) {}

}

进一步看看 disableForceDark 使用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51










void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {

if (CC_LIKELY(!info || info->disableForceDark)) {
return;
}


auto usage = usageHint();
const auto& children = mDisplayList->mChildNodes;
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) {

SkRect drawn = SkRect::MakeEmpty();
for (auto iter = children.rbegin(); iter != children.rend(); ++iter) {
const auto& child = iter->getRenderNode();

SkRect bounds = SkRect::MakeXYWH(child->stagingProperties().getX(), child->stagingProperties().getY(),
child->stagingProperties().getWidth(), child->stagingProperties().getHeight());
if (bounds.contains(drawn)) {

child->setUsageHint(UsageHint::Background);
}
drawn.join(bounds);
}
}


mDisplayList->mDisplayList.applyColorTransform(
usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

void DisplayListData::applyColorTransform(ColorTransform transform) {


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;

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]) {

fn(op, args...);
}
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 宏定义展开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template <class T>
constexpr color_transform_fn colorTransformForOp() {
if


constexpr(has_paint<T> && has_palette<T>) {

return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);

transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette);
};
}
else if

constexpr(has_paint<T>) {
return [](const void* opRaw, ColorTransform transform) {
const T* op = reinterpret_cast<const T*>(opRaw);

transformPaint(transform, const_cast<SkPaint*>(&(op->paint)));
};
}
else {

return nullptr;
}
}

static const color_transform_fn color_transform_fns[] = {

colorTransformForOp<Flush>
...
};

让我们再一次看看 map 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]) {

fn(op, args...);
}
ptr += skip;
}
}

贴了一大段代码,虽然代码中已经包含了注释,但还是可能比较晕,我们先来整理下:

  • CanvasContext.mUseForceDark 只会影响 TreeInfo.disableForceDark 的初始化
  • TreeInfo.disableForceDark 若大于 0,RenderNode 在执行 handleForceDark 就会直接退出
  • handleForceDark 方法里会根据 UsageHint 类型,对所有 op 中的 paint 颜色进行变换,如果是绘制图片,则叠加一个反转的 colorfilter。变换策略有:Dark、Light

接下来让我们来看 paint 和 colorfilter 的变色实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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;

if (paint.getColorFilter()->asColorMode(&color, &mode)) {

color = transformColor(transform, color);
paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode));
}
}
}

逻辑很简单,就是对颜色进行变换,进一步看看变色逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

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;
}
}


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;
}
}

static SkColor transformColor(ColorTransform transform, SkColor color) {
switch (transform) {
case ColorTransform::Light:
return makeLight(color);
case ColorTransform::Dark:
return makeDark(color);
default:
return color;
}
}

到此,对 paint 的变换结束,看来无非就是反转明度。

再来看看对图片的变换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) {

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;

paint->setColorFilter(SkHighContrastFilter::Make(config)->makeComposed(paint->refColorFilter()));
}
return shouldInvert;
}

终于,bitmap 的变换也分析完了,呼~

4.2. View 级别 forceDarkAllowed

但是,还没完呢~还记得我们最开始说的,除了 Theme 级别,还有一个 View 级别的 forceDarkAllowed,通过 View 级别 forceDarkAllowed 可以关掉它及它的子 view 的夜间模式开关。依然从 java 层看下去哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

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:

mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
break;
}
}
}
}


public final class RenderNode {
public boolean setForceDarkAllowed(boolean allow) {

return nSetAllowForceDark(mNativeRenderNode, allow);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

static jboolean android_view_RenderNode_setAllowForceDark(jlong renderNodePtr, jboolean allow) {
return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC);
}


class ANDROID_API RenderProperties {
public:
bool setAllowForceDark(bool allow) {

return RP_SET(mPrimitiveFields.mAllowForceDark, allow);
}

bool getAllowForceDark() const {
return mPrimitiveFields.mAllowForceDark;
}
}

和 Theme 级别的一样,仅仅只是设置到变量中而已,关键是要看哪里使用这个变量,经过查找,我们发现,它的使用同样在 RenderNode 的 prepareTreeImpl 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {

...




if (!mProperties.getAllowForceDark()) {
info.disableForceDark++;
}

prepareLayer(info, animatorDirtyMask);
if (info.mode == TreeInfo::MODE_FULL) {

pushStagingDisplayListChanges(observer, info);
}

if (mDisplayList) {
info.out.hasFunctors |= mDisplayList->hasFunctor();

bool isDirty = mDisplayList->prepareListAndChildren(
observer, info, childFunctorsNeedLayer,
[](RenderNode* child, TreeObserver& observer, TreeInfo& info,
bool functorsNeedLayer) {
child->prepareTreeImpl(observer, info, functorsNeedLayer);
});
if (isDirty) {
damageSelf(info);
}
}

...



if (!mProperties.getAllowForceDark()) {
info.disableForceDark--;
}
...
}

5. 总结

本文到目前为止,总算把 Android Q 夜间模式实现原理梳理了一遍,总的来说实现不算复杂,说白了就是把 paint 中的颜色转换一下或者叠加一个 colorfilter,虽然中间还有关联知识没有细说,如 RenderThread、DisplayList、RenderNode 等图形相关的概念,限于文章大小,请读者自行了解

另外,由于水平有限,难免文中有错漏之处,若哪里写的不对,请大家及时指出,蟹蟹啦~