背景

从Android10(API 29)开始,在原有的主题适配的基础上,Google开始提供了Force Dark机制,在系统底层直接对颜色和图片进行转换处理,原生支持深色模式。深色模式可以节省电量、改善弱势及强光敏感用户的可视性,并能在环境亮度较暗的时候保护视力,更是夜间活跃用户的强烈需求。对深色模式的适配有利于提升用户口碑。

深色模式在安卓上可以分为以下四种场景:

1. 强制深色模式

2. 强制浅色模式

3. 跟随系统

4. 低电量自动切换深色

以下将介绍如何设置深色模式以及如何对深色模式进行适配。

1

资源配置限定符

我们常见的需要设置的资源有drawable、layout、mipmap和values等,对于这些资源,我们可以用一些限定符来表示提供一些备用资源,例如drawable-xhdpi表示超密度屏幕使用的资源,或者layout-land表示横向状态使用的布局。

同样的深色模式可以使用资源的限定符-night来表示在深色模式中使用的资源。如下图所示:

使用了-night限定符的文件夹里面的资源我们称为night资源,没有使用-night限定符的资源我们称为notnight资源。

其中drawable-night-xhdpi可以放置对应超密度屏幕使用的深色模式的图片,values-night可以声明对应深色模式使用的色值和主题。

所有的资源限定符定义以及添加的顺序(例如-night必须在-xhdpi之前)可查看应用资源概览中的配置限定符名称表。

2

深色模式判断&设置

判断当前是否深色模式

Configuration.uiMode 有三种NIGHT的模式:

1. UI_MODE_NIGHT_NO 表示当前使用的是notnight模式资源。

2. UI_MODE_NIGHT_YES 表示当前使用的是night模式资源。

3. UI_MODE_NIGHT_UNDEFINED 表示当前没有设置模式。

可以通过以下的代码来判断当前是否处于深色模式:

/*** 判断当前是否深色模式** @return 深色模式返回 true,否则返回false*/
fun isNightMode(): Boolean {return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {Configuration.UI_MODE_NIGHT_YES -> trueelse -> false}
}

Tips: 对于一些从网络接口服务获取的需要对深色模式区分的色值或者图片,可以使用上述的判断来获取对应的资源。

判断当前深色模式场景

通过AppCompatDelegate.getDefaultNightMode())可以获取五种深色模式场景:

1. MODE_NIGHT_AUTO_BATTERY 低电量模式自动开启深色模式。

2. MODE_NIGHT_FOLLOW_SYSTEM 跟随系统开启和关闭深色模式(默认)。

3. MODE_NIGHT_NO 强制使用notnight资源,表示非深色模式。

4. MODE_NIGHT_YES 强制使用night资源。

5. MODE_NIGHT_UNSPECIFIED 配合 setLocalNightMode(int)) 使用,表示由Activity通过AppCompactActivity.getDelegate()来单独设置页面的深色模式,不设置全局模式。

模式设置

深色模式设置可以从三个层级设置,分别是系统层、Applcation层以及Activity层。底层的设置会覆盖上层的设置,例如系统设置了深色模式,但是Application设置了浅色模式,那么应用会显示浅色主题。

系统层是指系统设置中,根据不同产商的手机,可以在设置->显示中修改系统为深色模式。

Application层通过AppCompatDelegate.setDefaultNightMode()设置深色模式。

Activity层通过getDelegate().setLocalNightMode())设置深色模式。

当深色模式改变时,Activity会重建,如果不希望Activity重建,可以在AndroidManifest.xml中对对应的Activity设置android:configChanges="uiMode",不过设置之后页面的颜色改变需要Activity在中通过监听onConfigurationChanged来动态改变。

通过AppCompatDelegate.setDefaultNightMode(int))可以设置深色模式,源码如下:

public static void setDefaultNightMode(@NightMode int mode) {if (DEBUG) {Log.d(TAG, String.format("setDefaultNightMode. New:%d, Current:%d",mode, sDefaultNightMode));}switch (mode) {case MODE_NIGHT_NO:case MODE_NIGHT_YES:case MODE_NIGHT_FOLLOW_SYSTEM:case MODE_NIGHT_AUTO_TIME:case MODE_NIGHT_AUTO_BATTERY:if (sDefaultNightMode != mode) {sDefaultNightMode = mode;applyDayNightToActiveDelegates();}break;default:Log.d(TAG, "setDefaultNightMode() called with an unknown mode");break;}
}

从源码可以看出设置 MODE_NIGHT_UNSPECIFIED 模式是不会生效的。

Tips:注意,深色模式变化会导致Activity重建。

3

适配方案

自定义适配

1. 主题

将Application和Activity的主题修改为集成自Theme.AppCompat.DayNight或者Theme.MaterialComponents.DayNight,就可以对于大部分的控件得到较好的深色模式支持。我们看下DayNight主题的定义:

res/values/values.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2">        <!-- ... --><style name="Theme.AppCompat.DayNight" parent="Theme.AppCompat.Light"/><style name="Theme.AppCompat.DayNight.DarkActionBar" parent="Theme.AppCompat.Light.DarkActionBar"/><style name="Theme.AppCompat.DayNight.Dialog" parent="Theme.AppCompat.Light.Dialog"/><style name="Theme.AppCompat.DayNight.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert"/><style name="Theme.AppCompat.DayNight.Dialog.MinWidth" parent="Theme.AppCompat.Light.Dialog.MinWidth"/><style name="Theme.AppCompat.DayNight.DialogWhenLarge" parent="Theme.AppCompat.Light.DialogWhenLarge"/><style name="Theme.AppCompat.DayNight.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar"/><!-- ... -->
</resources>

res/values-night-v8/values-night-v8.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><style name="Theme.AppCompat.DayNight" parent="Theme.AppCompat"/><style name="Theme.AppCompat.DayNight.DarkActionBar" parent="Theme.AppCompat"/><style name="Theme.AppCompat.DayNight.Dialog" parent="Theme.AppCompat.Dialog"/><style name="Theme.AppCompat.DayNight.Dialog.Alert" parent="Theme.AppCompat.Dialog.Alert"/><style name="Theme.AppCompat.DayNight.Dialog.MinWidth" parent="Theme.AppCompat.Dialog.MinWidth"/><style name="Theme.AppCompat.DayNight.DialogWhenLarge" parent="Theme.AppCompat.DialogWhenLarge"/><style name="Theme.AppCompat.DayNight.NoActionBar" parent="Theme.AppCompat.NoActionBar"/><style name="ThemeOverlay.AppCompat.DayNight" parent="ThemeOverlay.AppCompat.Dark"/>
</resources>

res/values/values.xml

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:ns1="urn:oasis:names:tc:xliff:document:1.2" xmlns:ns2="http://schemas.android.com/tools"><!-- ... --><style name="Theme.MaterialComponents.DayNight" parent="Theme.MaterialComponents.Light"/><style name="Theme.MaterialComponents.DayNight.BottomSheetDialog" parent="Theme.MaterialComponents.Light.BottomSheetDialog"/><style name="Theme.MaterialComponents.DayNight.Bridge" parent="Theme.MaterialComponents.Light.Bridge"/><style name="Theme.MaterialComponents.DayNight.DarkActionBar" parent="Theme.MaterialComponents.Light.DarkActionBar"/><style name="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge" parent="Theme.MaterialComponents.Light.DarkActionBar.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog" parent="Theme.MaterialComponents.Light.Dialog"/><style name="Theme.MaterialComponents.DayNight.Dialog.Alert" parent="Theme.MaterialComponents.Light.Dialog.Alert"/><style name="Theme.MaterialComponents.DayNight.Dialog.Alert.Bridge" parent="Theme.MaterialComponents.Light.Dialog.Alert.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.Bridge" parent="Theme.MaterialComponents.Light.Dialog.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.FixedSize" parent="Theme.MaterialComponents.Light.Dialog.FixedSize"/><style name="Theme.MaterialComponents.DayNight.Dialog.FixedSize.Bridge" parent="Theme.MaterialComponents.Light.Dialog.FixedSize.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.MinWidth" parent="Theme.MaterialComponents.Light.Dialog.MinWidth"/><style name="Theme.MaterialComponents.DayNight.Dialog.MinWidth.Bridge" parent="Theme.MaterialComponents.Light.Dialog.MinWidth.Bridge"/><style name="Theme.MaterialComponents.DayNight.DialogWhenLarge" parent="Theme.MaterialComponents.Light.DialogWhenLarge"/><style name="Theme.MaterialComponents.DayNight.NoActionBar" parent="Theme.MaterialComponents.Light.NoActionBar"/><style name="Theme.MaterialComponents.DayNight.NoActionBar.Bridge" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge"/><!-- ... -->
</resources>

res/values-night-v8/values-night-v8.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><style name="Theme.MaterialComponents.DayNight" parent="Theme.MaterialComponents"/><style name="Theme.MaterialComponents.DayNight.BottomSheetDialog" parent="Theme.MaterialComponents.BottomSheetDialog"/><style name="Theme.MaterialComponents.DayNight.Bridge" parent="Theme.MaterialComponents.Bridge"/><style name="Theme.MaterialComponents.DayNight.DarkActionBar" parent="Theme.MaterialComponents"/><style name="Theme.MaterialComponents.DayNight.DarkActionBar.Bridge" parent="Theme.MaterialComponents.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog" parent="Theme.MaterialComponents.Dialog"/><style name="Theme.MaterialComponents.DayNight.Dialog.Alert" parent="Theme.MaterialComponents.Dialog.Alert"/><style name="Theme.MaterialComponents.DayNight.Dialog.Alert.Bridge" parent="Theme.MaterialComponents.Dialog.Alert.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.Bridge" parent="Theme.MaterialComponents.Dialog.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.FixedSize" parent="Theme.MaterialComponents.Dialog.FixedSize"/><style name="Theme.MaterialComponents.DayNight.Dialog.FixedSize.Bridge" parent="Theme.MaterialComponents.Dialog.FixedSize.Bridge"/><style name="Theme.MaterialComponents.DayNight.Dialog.MinWidth" parent="Theme.MaterialComponents.Dialog.MinWidth"/><style name="Theme.MaterialComponents.DayNight.Dialog.MinWidth.Bridge" parent="Theme.MaterialComponents.Dialog.MinWidth.Bridge"/><style name="Theme.MaterialComponents.DayNight.DialogWhenLarge" parent="Theme.MaterialComponents.DialogWhenLarge"/><style name="Theme.MaterialComponents.DayNight.NoActionBar" parent="Theme.MaterialComponents.NoActionBar"/><style name="Theme.MaterialComponents.DayNight.NoActionBar.Bridge" parent="Theme.MaterialComponents.NoActionBar.Bridge"/><style name="ThemeOverlay.MaterialComponents.DayNight.BottomSheetDialog" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog"/><style name="Widget.MaterialComponents.ActionBar.PrimarySurface" parent="Widget.MaterialComponents.ActionBar.Surface"/><style name="Widget.MaterialComponents.AppBarLayout.PrimarySurface" parent="Widget.MaterialComponents.AppBarLayout.Surface"/><style name="Widget.MaterialComponents.BottomAppBar.PrimarySurface" parent="Widget.MaterialComponents.BottomAppBar"/><style name="Widget.MaterialComponents.BottomNavigationView.PrimarySurface" parent="Widget.MaterialComponents.BottomNavigationView"/><style name="Widget.MaterialComponents.TabLayout.PrimarySurface" parent="Widget.MaterialComponents.TabLayout"/><style name="Widget.MaterialComponents.Toolbar.PrimarySurface" parent="Widget.MaterialComponents.Toolbar.Surface"/>
</resources>

Tips: MaterialComponents.Bridge继承自AppCompat主题,并增加了Material Components的主题属性,如果项目之前是用的AppCompat,那么使用对应的Bridge主题可以快速切换到Material Design。

从上面的分析可以看出,DayNight就是在values以及values-night中分别定义了浅色和深色的主题。如果我们的主题直接继承DayNight主题,那么就不需要重复地声明对应的night主题资源了。

如果我们想对深色模式主题添加自定义属性,那么我们可以不继承DayNight主题,并显示地声明主题对应的night资源,例如:

res/values/themes.xml

<style name="Theme.MyApp" parent="Theme.MaterialComponents.Light"><!-- ... --><item name="android:windowLightStatusBar">true</item>
</style>

res/values-night/themes.xml

<style name="Theme.MyApp" parent="Theme.MaterialComponents"><!-- ... --><item name="android:windowLightStatusBar">false</item>
</style>

Tips: 若需要动态修改主题要在调用inflate之前调用,否则不会生效。

2. 色值

主题切换颜色

除了定义不同模式使用不同的主题,我们还可以对主题设置自定义的色值。在设置主题色值之前,我们先了解一下Android主题的颜色系统。

• colorPrimary:主要品牌颜色,一般用于ActionBar背景。

• colorPrimaryDark:默认用于顶部状态栏和底部导航栏。

 colorPrimaryVariant:主要品牌颜色的可选颜色。

 colorSecondary:第二品牌颜色。

• colorSecondaryVariant:第二品牌颜色的可选颜色。

• colorPrimarySurface:对应Light主题指向colorPrimary,Dark主题指向colorSurface

• colorOn[Primary, Secondary, Surface …],在Primary等这些背景的上面内容的颜色,例如ActioBar上面的文字颜色。

 colorAccent:默认设置给colorControlActivated,一般是主要品牌颜色的明亮版本补充。

• colorControlNormal:图标和控制项的正常状态颜色。

• colorControlActivated:图标和控制项的选中颜色(例如Checked或者Switcher)。

• colorControlHighlight:点击高亮效果(ripple或者selector)。

 colorButtonNormal:按钮默认状态颜色。

• colorSurface:cards, sheets, menus等控件的背景颜色。

 colorBackground:页面的背景颜色。

• colorError:展示错误的颜色。

• textColorPrimary:主要文字颜色。

 textColorSecondary:可选文字颜色。

Tips: 当某个属性同时可以通过 ?attr/xxx 或者?android:attr/xxx获取时,最好使用?attr/xxx,因为?android:attr/xxx是通过系统获取,而?attr/xxx是通过静态库类似于AppCompat 或者 Material Design Component引入的。使用非系统版本的属性可以提高平台通用性。

如果需要自定义主题颜色,我们可以对颜色分别定义notnight和night两份,放在values以及values-night资源文件夹中,并在自定义主题时,传入给对应的颜色属性。例如:

res/values/styles.xml

<resources><style name="DayNightAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar.Bridge"><item name="colorPrimary">@color/color_bg_1</item><item name="colorPrimaryDark">@color/color_bg_1</item><item name="colorAccent">@color/color_main_1</item></style>
</resources>

res/values/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><color name="color_main_1">#4D71FF</color><color name="color_bg_1">#FFFFFF</color><color name="color_text_0">#101214</color><color name="color_light">#E0A62E</color>
</resources>

res/values-night/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><color name="color_main_1">#FF584D</color><color name="color_bg_1">#0B0C0D</color><color name="color_text_0">#F5F7FA</color><color name="color_light">#626469</color>
</resources>

控件切换颜色

同样的,我们可以在布局的XML文件中直接使用定义好的颜色值,例如:


<TextView android:id="@+id/auto_color_text"android:text="自定义变色文字"android:background="@drawable/bg_text"android:textColor="@color/color_text_0" />
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><stroke android:color="@color/color_text_0" android:width="2dp"/><solid android:color="@color/color_bg_1" />
</shape>

这样这个文字就会在深色模式中展示为黑底白字,在非深色模式中展示为白底黑字。

动态设置颜色

如果需要代码设置颜色,如果色值已经设置过notnight和night两份,那么直接设置颜色就可以得到深色模式变色效果。


auto_color_text.setTextColor(ContextCompat.getColor(this, R.color.color_text_0))

如果色值是从服务接口获取,那么可以使用上述深色模式的判断设置。


auto_color_text.setTextColor(if (isNightMode()) {Color.parseColor(darkColorFromNetwork)
} else {Color.parseColor(colorFromNetwork)
})

3. 图片&动画

普通图片&Gif图片

将图片分为明亮模式和深色模式两份,分别放置在drawable-night-xxx以及drawable-xxx文件夹中,并在view中直接使用即可,当深色模式切换时,会使用对应深色模式的资源。如下图所示:

<ImageView android:src="@drawable/round_fingerprint" />

Vector图片

在Vector资源定义时,通过指定画笔颜色来实现对深色模式的适配,例如:

<vector xmlns:android="http://schemas.android.com/apk/res/android"android:width="24dp"android:height="24dp"android:tint="@color/color_light"android:viewportWidth="24"android:viewportHeight="24"><pathandroid:fillColor="@android:color/white"android:pathData="M6.29,14.29L9,17v4c0,0.55 0.45,1 1,1h4c0.55,0 1,-0.45 1,-1v-4l2.71,-2.71c0.19,-0.19 0.29,-0.44 0.29,-0.71L18,10c0,-0.55 -0.45,-1 -1,-1L7,9c-0.55,0 -1,0.45 -1,1v3.59c0,0.26 0.11,0.52 0.29,0.7zM12,2c0.55,0 1,0.45 1,1v1c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L11,3c0,-0.55 0.45,-1 1,-1zM4.21,5.17c0.39,-0.39 1.02,-0.39 1.42,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41 -0.39,0.39 -1.02,0.39 -1.41,0l-0.72,-0.71c-0.39,-0.39 -0.39,-1.02 0,-1.41zM17.67,5.88l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71c-0.39,0.39 -1.02,0.39 -1.41,0 -0.39,-0.39 -0.39,-1.02 0,-1.41z" />
</vector>

其中android:tint为叠加颜色,@color/color_light已经分别定义好了notnight和night的色值。

Lottie

对于Lottie动画,我们可以使用Lottie的Dynamic Properties特性来针对深色模式进行颜色变化。例如我们有以下两个动画,左边是由颜色填充的机器人,右边是由描边生成的正在播放动画,我们可以调用LottieAnimationView.resolveKeyPath()方法获取动画的路径。

lottie_android_animate.addLottieOnCompositionLoadedListener {lottie_android_animate.resolveKeyPath(KeyPath("**")).forEach {Log.d(TAG, it.keysToString())}setupValueCallbacks()
}

对于机器小人打印的KeyPath如下:

2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [MasterController]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Head]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Head, Group 3]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Head, Group 3, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 2]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 2, Rectangle Path 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 2, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 1, Rectangle Path 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Blink, Rectangle 1, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Eyes]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Eyes, Group 3]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Eyes, Group 3, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [BeloOutlines]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [BeloOutlines, Group 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [BeloOutlines, Group 1, Stroke 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Shirt]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Shirt, Group 5]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Shirt, Group 5, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Body]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Body, Group 4]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [Body, Group 4, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftFoot]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftFoot, Group 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftFoot, Group 1, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightFoot]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightFoot, Group 2]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightFoot, Group 2, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave, LeftArm]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave, LeftArm, Group 6]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave, LeftArm, Group 6, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave, LeftArm, Group 5]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [LeftArmWave, LeftArm, Group 5, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightArm]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightArm, Group 6]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightArm, Group 6, Fill 1]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightArm, Group 5]
2020-09-10 15:30:55.762 29281-29281/com.shengj.androiddarkthemedemo D/DarkThemeDemo: [RightArm, Group 5, Fill 1]

我们抽取其中的某些形状来动态改变颜色,例如我们抽取左右手臂以及机器小人身上的T恤。

private fun setupValueCallbacks() {// 机器人右手臂val rightArm = KeyPath("RightArm", "Group 6", "Fill 1")// 机器人左手臂val leftArm = KeyPath("LeftArmWave", "LeftArm", "Group 6", "Fill 1")// 机器人T恤val shirt = KeyPath("Shirt", "Group 5", "Fill 1")// 设置右手臂颜色lottie_android_animate.addValueCallback(rightArm, LottieProperty.COLOR) {ContextCompat.getColor(this, R.color.color_main_1)}// 设置左手臂颜色lottie_android_animate.addValueCallback(shirt, LottieProperty.COLOR) {ContextCompat.getColor(this, R.color.color_light)}// 设置T恤颜色lottie_android_animate.addValueCallback(leftArm, LottieProperty.COLOR) {ContextCompat.getColor(this, R.color.color_custom)}// 播放动画描边颜色lottie_playing_animate.addValueCallback(KeyPath("**"), LottieProperty.STROKE_COLOR) {ContextCompat.getColor(this, R.color.color_text_0)}
}

由于color_main_1、color_light以及color_custom都已经定义过深色模式和明亮模式的色值,因此在深色模式切换时,Lottie动画的这个机器小人的左右手臂和T恤颜色会随着深色模式切换而变化。

同样的对于播放动画,我们也可以设置描边颜色,来达到深色模式切换的效果。

网络获取图片

对于网络获取的图片,可以让服务接口分别给出明亮模式和深色模式两套素材,然后根据上述的深色模式判断来进行切换。

Glide.with(this).load(if(isNightMode() nightImageUrl else imageUrl)).into(imgView)

Force Dark

看到这里可能会有人有疑问,对于大型的项目而言,里面已经hardcore了很多的颜色值,并且很多图片都没有设计成深色模式的,那做深色模式适配是不是一个不可能完成的任务呢?答案是否定的。对于大型项目而言,除了对所有的颜色和图片定义night资源的自定义适配方法外,我们还可以对使用Light风格主题的页面进行进行强制深色模式转换。

我们可以分别对主题和View设置强制深色模式。对于主题,在Light主题中设置android:forceDarkAllowed,例如:

<style name="LightAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge"><!-- ... --><item name="android:forceDarkAllowed">true</item>
</style>

对于View,设置View.setForceDarkAllowed(boolean))或者xml来设置是否支持Force Dark,默认值是true。


<Viewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:forceDarkAllowed="false"/>

这里需要注意的是,Force Dark的设置有以下几个规则:

1. 要强制深色模式生效必须开启硬件加速(默认开启)

2. 主题设置的Force Dark仅对Light的主题有效,对非Light的主题不管是设置android:forceDarkAllowed为true或者设置View.setForceDarkAllowed(true)都是无效的。

3. 父节点设置了不支持Force Dark,那么子节点再设置支持Force Dark无效。例如主题设置了android:forceDarkAllowed为false,则View设置View.setForceDarkAllowed(true)无效。同样的,如果View本身设置了支持Force Dark,但是其父layout设置了不支持,那么该View不会执行Force Dark

4. 子节点设置不支持Force Dark不受父节点设置支持Force Dark影响。例如View设置了支持Force Dark,但是其子Layout设置了不支持,那么子Layout也不会执行Force Dark。

Tips:一个比较容易记的规则就是不支持Force Dark优先,View 的 Force Dark设置一般会设置成 false,用于排除某些已经适配了深色模式的 View。

下面我们从源码出发来理解Force Dark的这些行为,以及看看系统是怎么实现Force Dark的。

Tips:善用 https://cs.android.com/ 源码搜索网站可以方便查看系统源码。

1. 主题

从主题设置的forceDarkAllowed入手查找,可以找到

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) {// 获取Force Dark的系统默认值boolean forceDarkAllowedDefault =SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);// 判断主题是否浅色主题 并且 判断主题设置的forceDarkAlloweduseAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)&& a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);a.recycle();}// 将是否强制使用深色模式赋值给Renderer层if (mAttachInfo.mThreadedRenderer.setForceDark(useAutoDark)) {// TODO: Don't require regenerating all display lists to apply this settinginvalidateWorld(mView);}
}

而这个方法正式在ViewRootImpl.enableHardwareAcceleration()方法中调用的,因此可以得到第一个结论:强制深色模式只在硬件加速下生效。由于userAutoDark变量会判断当前主题是否为浅色,因此可以得到第二个结论:强制深色模式只在浅色主题下生效。直到这一步的调用链如下:

mAttachInfo.mThreadedRenderer为ThreadRenderer,继承自HardwareRenderer,指定了接下来的渲染操作由RanderThread执行。继续跟踪setForceDark()方法:

frameworks/base/graphics/java/android/graphics/HardwareRenderer.java


public boolean setForceDark(boolean enable) {// 如果强制深色模式变化if (mForceDark != enable) {mForceDark = enable;// 调用native层设置强制深色模式逻辑nSetForceDark(mNativeProxy, enable);return true;}return false;
}private static native void nSetForceDark(long nativeProxy, boolean enabled);

查找nSetForceDark()方法

frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cpp

static const JNINativeMethod gMethods[] = {// ... // 在Android Runtime启动时,通过JNI动态注册{ "nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark },{ "preload", "()V", (void*)android_view_ThreadedRenderer_preload },
};

查找android_view_ThreadedRenderer_setForceDark()方法

frameworks/base/libs/hwui/jni/android_graphics_HardwareRenderer.cpp

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

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

void RenderProxy::setForceDark(bool enable) {// 调用CanvasContext的setForceDark方法mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}

frameworks/base/libs/hwui/renderthread/CanvasContext.h

// Force Dark的默认值是false
bool mUseForceDark = false;
// 设置mUseForceDark标志
void setForceDark(bool enable) { mUseForceDark = enable; }
bool useForceDark() {return mUseForceDark;
}

接着查找调用userForceDark()方法的地方

frameworks/base/libs/hwui/TreeInfo.cpp

TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext): mode(mode), prepareTextures(mode == MODE_FULL), canvasContext(canvasContext)// 设置disableForceDark变量, disableForceDark(canvasContext.useForceDark() ? 0 : 1), screenSize(canvasContext.getNextFrameSize()) {}}  // namespace android::uirenderer

frameworks/base/libs/hwui/TreeInfo.h

class TreeInfo {
public:// ...int disableForceDark;// ...
};

到了这里,可以看出,当设置了Force Dark之后,最终会设置到TreeInfo类中的disableForceDark变量,如果没有设置主题的Force Dark,那么根据false的默认值,disableForceDark变量会别设置成1,如果设置了使用强制深色模式,那么disableForceDark会变成0。

这个变量最终会用在RenderNode的RenderNode.handleForceDark()过程中,到达的流程如下图:

frameworks/base/libs/hwui/RenderNode.cpp

void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {// ...// 同步正在处理的RenderNode Property变化if (info.mode == TreeInfo::MODE_FULL) {pushStagingPropertiesChanges(info);}// 如果当前View不允许被ForceDark,那么info.disableForceDark值+1if (!mProperties.getAllowForceDark()) {info.disableForceDark++;}// ...// 同步正在处理的Render Node的Display List,实现具体深色的逻辑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) {// 递归调用子节点的prepareTreeImpl。// 递归调用之前,若父节点不允许强制深色模式,disableForceDark已经不为0,//     子节点再设置允许强制深色模式不会使得disableForceDark的值减少,//     因此有第三个规则:父节点设置了不允许深色模式,子节点再设置允许深色模式无效。// 同样的,递归调用之前,若父节点允许深色模式,disableForceDark为0,//     子节点再设置不允许强制深色模式,则disableForceDark值还是会++,不为0//     因此有第四个规则:子节点设置不允许强制深色模式不受父节点设置允许强制深色模式影响。child->prepareTreeImpl(observer, info, functorsNeedLayer);});if (isDirty) {damageSelf(info);}}pushLayerUpdate(info);// 递归结束后将之前设置过+1的值做回退-1恢复操作,避免影响其他兄弟结点的深色模式值判断if (!mProperties.getAllowForceDark()) {info.disableForceDark--;}info.damageAccumulator->popTransform();
}void RenderNode::pushStagingDisplayListChanges(TreeObserver& observer, TreeInfo& info) {// ...// 同步DisplayListsyncDisplayList(observer, &info);// ...
}void RenderNode::syncDisplayList(TreeObserver& observer, TreeInfo* info) {// ...if (mDisplayList) {WebViewSyncData syncData {// 设置WebViewSyncData的applyForceDark.applyForceDark = info && !info->disableForceDark};mDisplayList->syncContents(syncData);// 强制执行深色模式执行handleForceDark(info);}
}void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {if (CC_LIKELY(!info || info->disableForceDark)) {// 如果disableForceDark不为0,关闭强制深色模式,则直接返回return;}auto usage = usageHint();const auto& children = mDisplayList->mChildNodes;// 如果有文字表示是前景策略if (mDisplayList->hasText()) {usage = UsageHint::Foreground;}if (usage == UsageHint::Unknown) {// 如果子节点大于1或者第一个子节点不是背景,那么设置为背景策略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 checkSkRect 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 childrenSkRect 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 backgroundchild->setUsageHint(UsageHint::Background);}drawn.join(bounds);}}// 根据前景还是背景策略对颜色进行提亮或者加深mDisplayList->mDisplayList.applyColorTransform(usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}

Tips:View的绘制会根据VSYNC信号,将UI线程的Display List树同步到Render线程的Display List树,并通过生产者消费者模式将layout信息放置到SurfaceFlinger中,并最后交给Haredware Composer进行合成绘制。具体View渲染逻辑见参考章节的15~19文章列表。

frameworks/base/libs/hwui/RecordingCanvas.cpp

void DisplayListData::applyColorTransform(ColorTransform transform) {// 使用transform作为参数执行color_transform_fns函数组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,并调用对应类型的colorTransformForOp函数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 nullptrsfn(op, args...);        // to avoid the overhead of a pointless call.}ptr += skip;}
}typedef void (*color_transform_fn)(const void*, ColorTransform);#define X(T) colorTransformForOp<T>(),
static const color_transform_fn color_transform_fns[] = {// 相当于 colorTransformForOp<Flush>()X(Flush)X(Save)X(Restore)X(SaveLayer)X(SaveBehind)X(Concat44)X(Concat)X(SetMatrix)X(Scale)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)X(DrawWebView)
};
#undef Xstruct DrawImage final : Op {static const auto kType = Type::DrawImage;DrawImage(sk_sp<const SkImage>&& image, SkScalar x, SkScalar y, const SkPaint* paint,BitmapPalette palette): image(std::move(image)), x(x), y(y), palette(palette) {if (paint) {this->paint = *paint;}}sk_sp<const SkImage> image;SkScalar x, y;// 这里SK指代skia库对象SkPaint paint;BitmapPalette palette;void draw(SkCanvas* c, const SkMatrix&) const { c->drawImage(image.get(), x, y, &paint); }
};template <class T>
constexpr color_transform_fn colorTransformForOp() {if// 如果类型T有paint变量,并且有palette变量constexpr(has_paint<T> && has_palette<T>) {// It's a bitmap(绘制Bitmap)// 例如对于一个DrawImage的OP,最终会调用到这里// opRaw对应DrawImage对象,transform为ColorTransform::Dark或者ColorTransform::Lightreturn [](const void* opRaw, ColorTransform transform) {// TODO: We should be const. Or not. Or just use a different map// Unclear, but this is the quick fixconst T* op = reinterpret_cast<const T*>(opRaw);transformPaint(transform, const_cast<SkPaint*>(&(op->paint)), op->palette);};}else ifconstexpr(has_paint<T>) {return [](const void* opRaw, ColorTransform transform) {// TODO: We should be const. Or not. Or just use a different map// Unclear, but this is the quick fix// 非Bitmap绘制const T* op = reinterpret_cast<const T*>(opRaw);transformPaint(transform, const_cast<SkPaint*>(&(op->paint)));};}else {return nullptr;}
}

frameworks/base/libs/hwui/CanvasTransform.cpp

这里进行具体的颜色转换逻辑,我们首先关注非Bitmap绘制的颜色转换。

// 非Bitmap绘制颜色模式转换
bool transformPaint(ColorTransform transform, SkPaint* paint) {applyColorTransform(transform, *paint);return true;
}// 非Bitmap绘制颜色模式转换
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) {// 线性渐变并且渐变颜色少于等于10个的情况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;}}}// 处理colorFilterif (paint.getColorFilter()) {SkBlendMode mode;SkColor color;// TODO: LRU this or something to avoid spamming new color mode filtersif (paint.getColorFilter()->asAColorMode(&color, &mode)) {// 对colorFilter颜色进行转换color = transformColor(transform, color);paint.setColorFilter(SkColorFilters::Blend(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) {// 将sRGB色彩模式转换成Lab色彩模式Lab lab = sRGBToLab(color);// 对亮度L维度取反float invertedL = std::min(110 - lab.L, 100.0f);if (invertedL > lab.L) {// 若取反后亮度变亮,则替换原来亮度lab.L = invertedL;// 重新转换为sRGB模式return LabToSRGB(lab, SkColorGetA(color));} else {return color;}
}// 后景色变暗
static SkColor makeDark(SkColor color) {// 将sRGB色彩模式转换成Lab色彩模式Lab lab = sRGBToLab(color);// 对亮度L维度取反float invertedL = std::min(110 - lab.L, 100.0f);if (invertedL < lab.L) {// 若取反后亮度变暗,则替换原来亮度lab.L = invertedL;// 重新转换为sRGB模式return LabToSRGB(lab, SkColorGetA(color));} else {return color;}
}

从代码中可以看出,深色模式应用之后,通过对sRGB色彩空间转换Lab色彩空间,并对表示亮度的维度L进行取反,并判断取反后前景色是不是更亮,后景色是不是更暗,若是的话就替换为原来的L,并再重新转换为sRGB色彩空间,从而实现反色的效果。

我们再来看对图片的强制深色模式处理:

// Bitmap绘制颜色模式转换
bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) {// 考虑加上filter之后图片的明暗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;// 设置skia反转亮度的filterconfig.fInvertStyle = SkHighContrastConfig::InvertStyle::kInvertLightness;paint->setColorFilter(SkHighContrastFilter::Make(config)->makeComposed(paint->refColorFilter()));}return shouldInvert;
}// 获取paint filter的palette值,若没有filter直接返回原来的palette
static BitmapPalette filterPalette(const SkPaint* paint, BitmapPalette palette) {// 如果没有filter color返回原来的paletteif (palette == BitmapPalette::Unknown || !paint || !paint->getColorFilter()) {return palette;}SkColor color = palette == BitmapPalette::Light ? SK_ColorWHITE : SK_ColorBLACK;// 获取filter color,并根据palette的明暗再叠加一层白色或者黑色color = paint->getColorFilter()->filterColor(color);// 根据将颜色转换为HSV空间,并返回是图片的亮度是亮还是暗return paletteForColorHSV(color);
}

从代码中可以看出,对于Bitmap类型的绘制,先判断原来绘制Bitmap的明暗度,如果原来绘制的图像较为明亮但是需要变暗,或者原来绘制的图像较为暗需要变明亮,则设置一个明亮度转换的filter到画笔paint中。

至此,对于主题级别的强制深色转换原理已经非常清晰。总结一下,就是需要对前景色变亮和背景色变暗,然后对于非Bitmap类型明暗变化采用的是将色值转换为Lab颜色空间进行明亮度转换,对于Bitmap类型的明暗变化采取设置亮度转换的filter进行

2. View

无论是设置View的xml的android:forceDarkAllowed属性,还是调用View.setForceDarkAllowed()最后还是调用到frameworks/base/core/java/android/view/View.java的mRenderNode.setForceDarkAllowed()方法。

frameworks/base/graphics/java/android/graphics/RenderNode.java

public boolean setForceDarkAllowed(boolean allow) {return nSetAllowForceDark(mNativeRenderNode, allow);
}

nSetAllowForceDark通过JNI调用到android_view_RenderNode_setAllowForceDarkNavtive方法中。

frameworks/base/libs/hwui/jni/android_graphics_RenderNode.cpp

static const JNINativeMethod gMethods[] = {// ...{ "nSetAllowForceDark",        "(JZ)Z", (void*) android_view_RenderNode_setAllowForceDark },// ...
};static jboolean android_view_RenderNode_setAllowForceDark(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr, jboolean allow) {return SET_AND_DIRTY(setAllowForceDark, allow, RenderNode::GENERIC);
}#define SET_AND_DIRTY(prop, val, dirtyFlag) \(reinterpret_cast<RenderNode*>(renderNodePtr)->mutateStagingProperties().prop(val) \? (reinterpret_cast<RenderNode*>(renderNodePtr)->setPropertyFieldsDirty(dirtyFlag), true) \: false)

最后这个是否允许深色模式的allow变量被设置到RenderProperties.h 中

frameworks/base/libs/hwui/RenderProperties.h

/** Data structure that holds the properties for a RenderNode*/
class ANDROID_API RenderProperties {
public:// ...// 设置View是否允许强制深色模式bool setAllowForceDark(bool allow) {return RP_SET(mPrimitiveFields.mAllowForceDark, allow);}// 获取View是否允许强制深色模式bool getAllowForceDark() const {return mPrimitiveFields.mAllowForceDark;}// ...
private:// Rendering propertiesstruct PrimitiveFields {// ...// 默认值为truebool mAllowForceDark = true;// ...} mPrimitiveFields;

我们回头看下上面分析过的RenderNode.cpp的prepareTreeImpl流程

frameworks/base/libs/hwui/RenderNode.cpp

// 经过了简化处理的prepareTreeImpl逻辑
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info) {// 如果当前View不允许被ForceDark,那么info.disableForceDark值+1if (!mProperties.getAllowForceDark()) {info.disableForceDark++;}// 同步正在处理的Render Node的Display List,实现具体深色的逻辑pushStagingDisplayListChanges(observer, info);mDisplayList->prepareListAndChildren([](RenderNode* child, TreeObserver& observer, TreeInfo& info) {// 递归调用子节点的prepareTreeImpl。// 递归调用之前,若父节点不允许强制深色模式,disableForceDark已经不为0,//     子节点再设置允许强制深色模式不会使得disableForceDark的值减少,//     因此有第三个规则:父节点设置了不允许深色模式,子节点再设置允许深色模式无效。// 同样的,递归调用之前,若父节点允许深色模式,disableForceDark为0,//     子节点再设置不允许强制深色模式,则disableForceDark值还是会++,不为0//     因此有第四个规则:子节点设置不允许强制深色模式不受父节点设置允许强制深色模式影响。child->prepareTreeImpl(observer, info);});// 递归结束后将之前设置过+1的值做回退-1恢复操作,避免影响其他兄弟结点的深色模式值判断if (!mProperties.getAllowForceDark()) {info.disableForceDark--;}
}

可以看出,设置View的forceDarkAllowed最终会设置到当前RenderNode的mProperties.allowForceDark属性中,并在RenderNode遍历的过程中影响深色模式的执行。

我们可以以下面的伪代码来更直观地了解深色模式执行的流程:

// 深色模式渲染伪代码
int disableDark = if (themeAllowDark) 0 else 1;void RenderNode(Node node) {if (!node.allowDark) {disableDark++;}if (disableDark == 0) forceDarkCurrentNode();for (child : node.children) {RenderNode(child)}if (!node.allowDark) {disableDark--;}
}

至此,我们分析完所有强制深色模式的原理。总结一下,主题默认不会强制深色,若主题设置了强制深色,则遍历View树对其节点进行强制深色转换。碰到某个View不希望被强制深色,则包括它和它的所有子节点都不会被强制深色

4

总结

到这里,我们了解了可以通过设置-night资源以及判断当前颜色模式来自定义切换主题、色值、图片和动画的颜色,也从源代码角度了解Force Dark的原理和生效规则。

Demo

上述提到的代码可以到这个Github项目下载。

https://github.com/shenguojun/AndroidDarkThemeDemo

参考

Google Developers - Dark Theme

Material Design - Dark Theme

Material Design - The color system

Android 10 暗黑模式适配,你需要知道的一切

Android 10 Dark Theme: Getting Started

Android styling: themes vs styles

Android styling: common theme attributes

Android Styling: prefer theme attributes

Lottie - Dynamic Properties

Lottie on Android: Part 3 — Dynamic properties

MIUI 深色模式适配说明

OPPO 暗色模式适配说明

Android Q深色模式源码解析

Moving to the Dark Side: Dark Theme Recap

Android应用程序UI硬件加速渲染环境初始化过程分析

Android应用程序UI硬件加速渲染的Display List构建过程分析

Android应用程序UI硬件加速渲染的Display List渲染过程分析

Drawn out: how Android renders (Google I/O ‘18)

深入理解Android的渲染机制

SKIA api

Android Code Search

带你实战Android深色模式,深入原理剖析

带你实战Android深色模式,深入原理剖析相关推荐

  1. Android深色模式适配原理分析,android应用开发

    return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI ...

  2. android自动切换暗色,超实用!Android 深色模式适配(可定时开启的APP内主题切换管理工具)...

    前言 前面分享了一篇"黑白化主题"的文,主要适用场景是不久就要到来的"清明"等时节或者是其他的国家公祭日什么的(一名成熟的程序员,要学会自己提产品需求). 今天 ...

  3. Redis哨兵模式(原理剖析)

    Redis哨兵模式(原理剖析) 前言 一.Sentinel模式概述 二.配置哨兵模式 1.哨兵配置 1.1 优点: 1.2 缺点: 总结 前言 在Redis中主从复制解决高可用的方案是:当主节点服务器 ...

  4. Android深色模式下,看不见字的解决办法

    你去看看,看不见字的那些是不是都没有设置字体颜色! 设置一下字体颜色就好啦! 对于AlertDialog,千万不要用它自己的,要我们自己写好view,然后设置view,不然的话,深色模式下,你是很难修 ...

  5. Android RollBack机制实现原理剖析

    功能介绍: 在Android 10.0中,Google新增加了个功能. 如果用户对新升级的APP不满意,可以通过"回到过去",回滚到旧版. 当然,如果新安装的apk出现了各种问题无 ...

  6. Android Zenmode/DND(勿扰模式) 实现原理剖析

    引言 Android手机越来越多的向着用户体验提升方面靠近,那么Zenmode就会变得越来越重要. 近年来,也有很多的新功能依赖于ZenMode去实现,也有很多专利在这个方面申请成功. 举两个简单的例 ...

  7. android okio使用方法,Android 开源框架 Okio 原理剖析

    Retrofit,OkHttp,Okio 是 Square 团队开源的安卓平台网络层三板斧,它们逐层分工,非常优雅地解决我们对网络请求甚至更广泛的 I/O 操作的需求.其中最底层的 Okio 堪称小而 ...

  8. android9有深色模式吗,深色模式还是黑色模式?微信把所有人都搞懵了

    原标题:深色模式还是黑色模式?微信把所有人都搞懵了 前一阵子,微信正式加入了对"深色模式"的支持,这也是除了Windows Phone 版本以外微信第一次从系统层面支持深色模式.虽 ...

  9. spark任务shell运行_Spark原理与实战(七)部署模式与运行机制

    导读:Spark的运行模式指的是Spark应用程序以怎样的方式运行,单节本地点运行还是多节点集群运行,自己进行资源调度管理还是依靠别人进行调度管理.Spark提供了多种多样,灵活多变的部署模式. 作者 ...

  10. Android夜间模式原理

    1.夜间模式怎么实现? 先看一个实现夜间模式的demo. 首先看MainActivity的代码: public class MainActivity extends Activity {private ...

最新文章

  1. 向下滚动页面导航悬浮
  2. 常见Web安全漏洞--------sql注入
  3. AIX 下磁盘 I/O 性能分析
  4. Oracle中row_number()、rank()、dense_rank() 的区别
  5. js时间搓化为今天明天_秋冬国产搓背神器!360°无死角,让你搓背不求人,太舒服了...
  6. 他,先后担任4所大学校长!
  7. 串行口通信c语言代码,问一下单片机串行口通信用c语言实现的问题
  8. (实用篇)浅谈PHP拦截器之__set()与__get()的理解与使用方法
  9. hashmap value占用空间大小_性能优化:为什么要使用SparseArray和ArrayMap替代HashMap?...
  10. SpringCloud教程汇总
  11. Struts2通配符
  12. 梯田油菜花海距杭州仅120公里
  13. 人工智能 - A*算法解决迷宫问题 附源码和可视化显示
  14. 面试常见问题回答技巧(70题)
  15. 高德地图自动获取当前位置可搜索可拖拽获得GPS和道路信息
  16. Android 展讯-关闭IP拨号功能
  17. 逻辑思维训练——排除法
  18. 网页游戏服务器搭建,如何搭建网页游戏服务器
  19. 都会演出连城诀—诺基亚N78决战入手入手了!
  20. 从金本位到法定货币:银行家世界观的

热门文章

  1. im服务器开源项目,Oschat IM 开源即时通讯项目介绍
  2. canvas 踩坑 * 小球弹性碰撞逻辑解析
  3. python实现excel转置问题
  4. 摆脱晚睡拖延症其实很简单……
  5. 04:AD采样【MSP430F5529】
  6. 谈谈对计算机网络的了解,对计算机网络的认识和了解
  7. php 阿里云短信接口 demo最新
  8. 怎么在Excel里输入可以打钩的选择框?
  9. WhatsApp Botim 筛号,筛选群发消息,WhatsApp、Botim协议
  10. 电脑磁盘分区数据丢失怎么办 磁盘分区丢失的数据怎么恢复