隨着 Android Q 發佈,「黑暗模式」或者說是「夜間模式」終於在此版本中獲得了支持,官方介紹見:developer.android.com/guide/topic…,再看看效果圖:html
其實這個功能魅族在兩年前就已支持,不得不說 Android 有點落後了,今天咱們就來看看原生是怎麼實現全局夜間模的吧java
從文檔上咱們能夠可知,打開夜間模式有三個方法:android
打開後,咱們會發現,除原生幾個應用生效外,其餘應用依然沒有變成深色主題,那麼應用該如何適配呢?官方提供了下面兩種方法:canvas
DayNight
主題<style name="AppTheme" parent="Theme.AppCompat.DayNight"> 複製代碼
或者繼承自數組
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> 複製代碼
繼承後,若是當前開啓了夜間模式,系統會自動從 night-qualified 中加載資源,因此應用的顏色、圖標等資源應儘可能避免硬編碼,而是推薦使用新增 attributes 指向不一樣的資源,如app
?android:attr/textColorPrimary
?attr/colorControlNormal
複製代碼
另外,若是應用但願主動切換夜間/日間模式,能夠經過 AppCompatDelegate.setDefaultNightMode()
接口主動切換less
若是應用不想本身去適配各類顏色,圖標等,能夠經過在主題中添加 android:forceDarkAllowed="true"
標記,這樣系統在夜間模式時,會強制改變應用顏色,自動進行適配(這個功能也是本文主要探討的)。不過若是你的應用自己使用的就是 DayNight
或 Dark Theme
,forceDarkAllowed 是不會生效的。ide
另外,若是你不但願某個 view 被強制夜間模式處理,則能夠給 view 添加 android:forceDarkAllowed="false"
或者 view.setForceDarkAllowed(false),設置以後,即便打開了夜間模式且主題添加了 forceDarkAllowed,該 view 也不會變深色。比較重要的一點是,這個接口只能關閉夜間模式,不能開啓夜間模式,也就是說,若是主題中沒有顯示聲明 forceDarkAllowed,view.setForceDarkAllowed(true)
是沒辦法讓 view 單獨變深色的。若是 view 關閉了夜間模式,那麼它的子 view 也會強制關閉夜間模式函數
總結以下:post
DayNight
或 Dark Theme
主題,則全部 forceDarkAllowed 都不生效經過繼承主題適配夜間模式的原理本質是根據 ui mode 加載 night-qualified 下是資源,這個並不是 Android Q 新增的東西,咱們這裏再也不描述。如今主要來看看 forceDarkAllowed 是如何讓系統變深色的。
既然一切的源頭都是 android:forceDarkAllowed
這個屬性,那咱們就從它入手吧,首先咱們要知道,上面咱們說的 android:forceDarkAllowed
實際上是分爲兩個用處,它們分別的定義以下:
frameworks/base/core/res/res/values/attrs.xml
<declare-styleable name="View">
<!-- <p>Whether or not the force dark feature is allowed to be applied to this View. <p>Setting this to false will disable the auto-dark feature on this View draws including any descendants. <p>Setting this to true will allow this view to be automatically made dark, however a value of 'true' will not override any 'false' value in its parent chain nor will it prevent any 'false' in any of its children. -->
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>
<declare-styleable name="Theme">
<!-- <p>Whether or not the force dark feature is allowed to be applied to this theme. <p>Setting this to false will disable the auto-dark feature on everything this theme is applied to along with anything drawn by any children of views using this theme. <p>Setting this to true will allow this view to be automatically made dark, however a value of 'true' will not override any 'false' value in its parent chain nor will it prevent any 'false' in any of its children. -->
<attr name="forceDarkAllowed" format="boolean" />
</declare-styleable>
複製代碼
一個是 View 級別的,一個是 Theme 級別的。
從上面的總結來看,Theme 級別的開關優先級是最高的,控制粒度也最大,咱們看看源碼裏面使用它的地方
// frameworks/base/core/java/android/view/ViewRootImpl.java
private void updateForceDarkMode() {
// 渲染線程爲空,直接返回
if (mAttachInfo.mThreadedRenderer == null) return;
// 系統是否打開了黑暗模式
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
if (useAutoDark) {
// forceDarkAllowed 默認值,開發者模式是否打開了強制 smart dark 選項
boolean forceDarkAllowedDefault =
SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
// useAutoDark = 使用淺色主題 && 主題中聲明的 forceDarkAllowed 值
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);
}
}
// frameworks/base/graphics/java/android/graphics/HardwareRenderer.java
public boolean setForceDark(boolean enable) {
if (mForceDark != enable) {
mForceDark = enable;
// native 代碼,mNativeProxy 實際上是 RenderThread 代理類的指針
nSetForceDark(mNativeProxy, enable);
return true;
}
return false;
}
複製代碼
這段代碼仍是比較簡單,判斷系統:
三者同時爲 true 時纔會設置夜間模式,而 updateForceDarkMode 調用的時機分別是在 ViewRootImpl#setView
和 ViewRootImpl#updateConfiguration
,也就是初始化和夜間模式切換的時候都會調用,確保夜間模式能及時啓用和關閉。繼續跟蹤 HardwareRenderer#setForceDark
發現,這是一個 native 方法,因此接下來讓咱們進入 native 世界,nSetForceDark 對應的實現位於
// frameworks/base/core/jni/android_view_ThreadedRenderer.cpp
static void android_view_ThreadedRenderer_setForceDark(JNIEnv* env, jobject clazz, jlong proxyPtr, jboolean enable) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
proxy->setForceDark(enable);
}
// frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
void RenderProxy::setForceDark(bool enable) {
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}
// frameworks/base/libs/hwui/renderthread/CanvasContext.h
class CanvasContext : public IFrameCallback {
public:
...
void setForceDark(bool enable) { mUseForceDark = enable; }
bool useForceDark() {
return mUseForceDark;
}
...
private:
...
// 默認關閉強制夜間模式
bool mUseForceDark = false;
...
};
複製代碼
最終就是設置了一個 CanvasContext 的變量值而已,什麼都尚未作,那麼這個變量值的做用是什麼,何時生效呢?咱們進一步查看使用的地方:
// 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()) {}
}
複製代碼
進一步看看 disableForceDark 使用的地方
// frameworks/base/libs/hwui/RenderNode.cpp
/** * 這個能夠說是核心方法了,handleForceDark 方法調用棧以下: * - RenderNode#prepareTreeImpl * - RenderNode#pushStagingDisplayListChanges * - RenderNode#syncDisplayList * - RenderNode#handleForceDark * * 而 RenderNode#prepareTree 是繪製的必經之路,每個節點都會走一遍這個流程 */
void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
// 若沒打開強制夜間模式,直接退出
if (CC_LIKELY(!info || info->disableForceDark)) {
return;
}
// 根據是否有文字、是否有子節點、子節點數量等狀況,得出當前 Node 屬於 Foreground 仍是 Background
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) {
// 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
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;
}
}
複製代碼
貼了一大段代碼,雖然代碼中已經包含了註釋,但仍是可能比較暈,咱們先來整理下:
接下來讓咱們來看 paint 和 colorfilter 的變色實現
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++) {
// 對 shader 中的顏色進行顏色變換
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)) {
// 對 colorfilter 中的顏色進行顏色變換
color = transformColor(transform, color);
paint.setColorFilter(SkColorFilter::MakeModeFilter(color, mode));
}
}
}
複製代碼
邏輯很簡單,就是對顏色進行變換,進一步看看變色邏輯:
// 提亮顏色
static SkColor makeLight(SkColor color) {
// 轉換成 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 lab = sRGBToLab(color);
// 對明度進行反轉,明度越高,反轉後越低
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL < lab.L) {
// 反轉後的明度低於原明度,則使用反轉後的明度
lab.L = invertedL;
// 使用 rgb 格式返回
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 的變換結束,看來無非就是反轉明度。
再來看看對圖片的變換:
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;
}
複製代碼
終於,bitmap 的變換也分析完了,呼~
可是,還沒完呢~~~還記得咱們最開始說的,除了 Theme 級別,還有一個 View 級別的 forceDarkAllowed,經過 View 級別 forceDarkAllowed 能夠關掉它及它的子 view 的夜間模式開關。依然從 java 層看下去哈
// rameworks/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:
// 注意,這個默認是 true 的
mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true));
break;
}
}
}
}
// frameworks/base/graphics/java/android/graphics/RenderNode.java
public final class RenderNode {
public boolean setForceDarkAllowed(boolean allow) {
// 又是 native 方法
return nSetAllowForceDark(mNativeRenderNode, allow);
}
}
複製代碼
// frameworks/base/core/jni/android_view_RenderNode.cpp
static jboolean android_view_RenderNode_setAllowForceDark(jlong renderNodePtr, jboolean allow) {
return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC);
}
// frameworks/base/libs/hwui/RenderProperties.h
class ANDROID_API RenderProperties {
public:
bool setAllowForceDark(bool allow) {
// 設置到 mPrimitiveFields.mAllowForceDark 變量中
return RP_SET(mPrimitiveFields.mAllowForceDark, allow);
}
bool getAllowForceDark() const {
return mPrimitiveFields.mAllowForceDark;
}
}
複製代碼
和 Theme 級別的同樣,僅僅只是設置到變量中而已,關鍵是要看哪裏使用這個變量,通過查找,咱們發現,它的使用一樣在 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 夜間模式實現原理梳理了一遍,總的來講實現不算複雜,說白了就是把 paint 中的顏色轉換一下或者疊加一個 colorfilter,雖然中間還有關聯知識沒有細說,如 RenderThread、DisplayList、RenderNode 等圖形相關的概念,限於文章大小,請讀者自行了解
另外,因爲水平有限,不免文中有錯漏之處,若哪裏寫的不對,請你們及時指出,蟹蟹啦~