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
View级别:
//frameworks/base/core/res/res/values/attrs.xml
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(),
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
constexpr color_transform_fn colorTransformForOp() {
if
// op 变量中是否同时包含 paint 及 palette 属性,若同时包含,则是绘制 Image 或者 VectorDrawable 的指令
// 参考:frameworks/base/libs/hwui/RecordingCanvas.cpp 中各 Op 的定义
constexpr(has_paint && has_palette) {
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) {
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
…
};
让我们再一次看看 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要讲完了,你看明白了吗?
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后
简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。
选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!
面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
!(备注:Android)**

最后
简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。
选定你想去的几家公司后,先去一些小的公司练练,学习下面试技巧,总结下,也算是熟悉下面试氛围,平时和同事或者产品PK时可以讲得头头是道,思路清晰至极,到了现场真的不一样,怎么描述你所做的一切,这绝对是个学术性问题!
面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。
金九银十面试季,跳槽季,整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
[外链图片转存中…(img-VEU3izY2-1712509595016)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!