1. 简介 随着 Android Q 发布,「黑暗模式」或者说是「夜间模式」终于在此版本中得到了支持,官方介绍见:https://developer.android.com/guide/topics/ui/look-and-feel/darktheme ,再看看效果图:
其实这个功能魅族在两年前就已支持,不得不说 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"
标记,这样系统在夜间模式时,会强制改变应用颜色,自动进行适配(这个功能也是本文主要探讨的)。不过如果你的应用本身使用的就是 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 都不生效
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#setView
和 ViewRootImpl#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 等图形相关的概念,限于文章大小,请读者自行了解
另外,由于水平有限,难免文中有错漏之处,若哪里写的不对,请大家及时指出,蟹蟹啦~