Android深色模式适配原理分析

布景

从Android10(API 29)开端,在原有的主题适配的基础上,Google开端供给了Force Dark机制,在体系底层直接对色彩和图片进行转化处理,原生支撑深色形式。深色形式能够节省电量、改进弱势及强光灵敏用户的可视性,并能在环境亮度较暗的时候保护视力,更是夜间活泼用户的激烈需求。对深色形式的适配有利于进步用户口碑。转载请注明来历「申国骏」

深色形式在安卓上能够分为以下四种场景:

  • 强制深色形式

  • 强制淡色形式

  • 跟从体系

  • 低电量主动切换深色

以下将介绍怎么设置深色形式以及怎么对深色形式进行适配。

资源配置限定符

咱们常见的需求设置的资源有drawablelayoutmipmapvalues等,关于这些资源,咱们能够用一些限定符来表明供给一些备用资源,例如drawable-xhdpi表明超密度屏幕运用的资源,或许layout-land表明横向状况运用的布局。

相同的深色形式能够运用资源的限定符-night来表明在深色形式中运用的资源。如下图所示:

Android深色模式适配原理分析

Android深色模式适配原理分析

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

其间drawable-night-xhdpi能够放置对应超密度屏幕运用的深色形式的图片,values-night能够声明对应深色形式运用的色值和主题。

全部的资源限定符界说以及增加的顺序(例如-night必须在-xhdpi之前)可检查运用资源概览中的配置限定符称号表。

深色形式判别&设置

判别当时是否深色形式

Configuration.uiMode 有三种NIGHT的形式

  • UI_MODE_NIGHT_NO 表明当时运用的是notnight形式资源
  • UI_MODE_NIGHT_YES 表明当时运用的是night形式资源
  • 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 -> true
    else -> false
  }
}

Tips: 关于一些从网络接口服务获取的需求对深色形式区分的色值或许图片,能够运用上述的判别来获取对应的资源。

判别当时深色形式场景

经过AppCompatDelegate.getDefaultNightMode()能够获取五种深色形式场景:

  • MODE_NIGHT_AUTO_BATTERY 低电量形式主动敞开深色形式
  • MODE_NIGHT_FOLLOW_SYSTEM 跟从体系敞开和封闭深色形式(默许)
  • MODE_NIGHT_NO 强制运用notnight资源,表明非深色形式
  • MODE_NIGHT_YES 强制运用night资源
  • MODE_NIGHT_UNSPECIFIED 合作 setLocalNightMode(int) 运用,表明由Activity经过AppCompactActivity.getDelegate()来独自设置页面的深色形式,不设置全局形式

形式设置

深色形式设置能够从三个层级设置,别离是体系层、Applcation层以及Activity层。底层的设置会掩盖上层的设置,例如体系设置了深色形式,可是Application设置了淡色形式,那么运用会显现淡色主题。

Android深色模式适配原理分析

体系层是指体系设置中,依据不同产商的手机,能够在设置->显现中修改体系为深色形式。

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重建。

适配计划

自界说适配

1. 主题

将Application和Activity的主题修改为集成自Theme.AppCompat.DayNight或许Theme.MaterialComponents.DayNight,就能够关于大部分的控件得到较好的深色形式支撑。咱们看下DayNight主题的界说:

Android深色模式适配原理分析

**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>

Android深色模式适配原理分析

**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主题的色彩体系。

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引入的。运用非体系版本的特点能够进步平台通用性。

假如需求自界说主题色彩,咱们能够对色彩别离界说notnightnight两份,放在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>

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

Android深色模式适配原理分析

Android深色模式适配原理分析

动态设置色彩

假如需求代码设置色彩,假如色值现已设置过notnightnight两份,那么直接设置色彩就能够得到深色形式变色作用。

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中直接运用即可,当深色形式切换时,会运用对应深色形式的资源。如下图所示:

Android深色模式适配原理分析

<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">
    <path
        android: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现已别离界说好了notnightnight的色值。 Tips: 为了节省图片占用的安装包大小,咱们能够尽量对图标都运用运用vector类型资源,详细能够在UI规划稿中导出SVG格局图片,然后在Andorid Studio中导入,详细操作步骤如下:

Android深色模式适配原理分析

Android深色模式适配原理分析

Lottie

关于Lottie动画,咱们能够运用Lottie的Dynamic Properties特性来针对深色形式进行色彩改变。例如咱们有以下两个动画,左边是由色彩填充的机器人,右边是由描边生成的正在播映动画,咱们能够调用LottieAnimationView.resolveKeyPath()办法获取动画的途径。

Android深色模式适配原理分析

Android深色模式适配原理分析

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_1color_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。

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

这儿需求注意的是,Force Dark的设置有以下几个规矩:

  1. 要强制深色形式收效必须敞开硬件加速(默许敞开)
  2. 主题设置的Force Dark仅对Light的主题有用,对非Light的主题不管是设置android:forceDarkAllowedtrue或许设置View.setForceDarkAllowed(true)都是无效的。
  3. 父节点设置了不支撑Force Dark,那么子节点再设置支撑Force Dark无效。例如主题设置了android:forceDarkAllowedfalse,则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:善用 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);
    // 判别主题是否淡色主题 而且 判别主题设置的forceDarkAllowed
    useAutoDark = 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 setting
    invalidateWorld(mView);
  }
}

而这个办法正式在ViewRootImpl.enableHardwareAcceleration()办法中调用的,因此能够得到榜首个定论:强制深色形式只在硬件加速下收效。因为userAutoDark变量会判别当时主题是否为淡色,因此能够得到第二个定论:强制深色形式只在淡色主题下收效。直到这一步的调用链如下:

Android深色模式适配原理分析

mAttachInfo.mThreadedRendererThreadRenderer,承继自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()进程中,抵达的流程如下图:

Android深色模式适配原理分析

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值+1
    if (!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) {
    // ...
    // 同步DisplayList
    syncDisplayList(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 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);
        }
    }
    // 依据远景仍是布景战略对色彩进行提亮或许加深
    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 nullptrs
            fn(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 X
struct 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::Light
            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
                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) {
                // 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;
            }
        }
    }
    // 处理colorFilter
    if (paint.getColorFilter()) {
        SkBlendMode mode;
        SkColor color;
        // TODO: LRU this or something to avoid spamming new color mode filters
        if (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反转亮度的filter
        config.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回来本来的palette
    if (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 properties
    struct PrimitiveFields {
        // ...
        // 默许值为true
        bool mAllowForceDark = true;
        // ...
    } mPrimitiveFields;

咱们回头看下上面剖析过的RenderNode.cppprepareTreeImpl流程

frameworks/base/libs/hwui/RenderNode.cpp

// 经过了简化处理的prepareTreeImpl逻辑
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info) {
    // 假如当时View不答应被ForceDark,那么info.disableForceDark值+1
    if (!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最终会设置到当时RenderNodemProperties.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不希望被强制深色,则包含它和它的全部子节点都不会被强制深色。

总结

到这儿,咱们了解了能够经过设置-night资源以及判别当时色彩形式来自界说切换主题、色值、图片和动画的色彩,也从源代码角度了解Force Dark的原理和收效规矩。

Demo

上述提到的代码能够到这个Github项目下载

参阅

  1. Google Developers – Dark Theme
  2. Material Design – Dark Theme
  3. Material Design – The color system
  4. Android 10 暗黑形式适配,你需求知道的全部
  5. Android 10 Dark Theme: Getting Started
  6. Android styling: themes vs styles
  7. Android styling: common theme attributes
  8. Android Styling: prefer theme attributes
  9. Lottie – Dynamic Properties
  10. Lottie on Android: Part 3 — Dynamic properties
  11. MIUI 深色形式适配说明
  12. OPPO 暗色形式适配说明
  13. Android Q深色形式源码解析
  14. Moving to the Dark Side: Dark Theme Recap
  15. Android运用程序UI硬件加速烘托环境初始化进程剖析
  16. Android运用程序UI硬件加速烘托的Display List构建进程剖析
  17. Android运用程序UI硬件加速烘托的Display List烘托进程剖析
  18. Drawn out: how Android renders (Google I/O ’18)
  19. 深入理解Android的烘托机制
  20. SKIA api
  21. Android Code Search