背景
從Android10(API 29)開始俭尖,在原有的主題適配的基礎(chǔ)上典蝌,Google開始提供了Force Dark機制阐斜,在系統(tǒng)底層直接對顏色和圖片進行轉(zhuǎn)換處理具垫,原生支持深色模式。深色模式可以節(jié)省電量、改善弱勢及強光敏感用戶的可視性,并能在環(huán)境亮度較暗的時候保護視力,更是夜間活躍用戶的強烈需求称龙。對深色模式的適配有利于提升用戶口碑。 轉(zhuǎn)載請注明來源「申國駿」
深色模式在安卓上可以分為以下四種場景:
強制深色模式
強制淺色模式
跟隨系統(tǒng)
低電量自動切換深色
以下將介紹如何設(shè)置深色模式以及如何對深色模式進行適配戳晌。
資源配置限定符
我們常見的需要設(shè)置的資源有drawable
鲫尊、layout
、mipmap
和values
等沦偎,對于這些資源疫向,我們可以用一些限定符來表示提供一些備用資源,例如drawable-xhdpi
表示超密度屏幕使用的資源豪嚎,或者layout-land
表示橫向狀態(tài)使用的布局搔驼。
同樣的深色模式可以使用資源的限定符-night
來表示在深色模式中使用的資源。如下圖所示:
使用了-night
限定符的文件夾里面的資源我們稱為night
資源侈询,沒有使用-night
限定符的資源我們稱為notnight
資源舌涨。
其中drawable-night-xhdpi
可以放置對應(yīng)超密度屏幕使用的深色模式的圖片,values-night
可以聲明對應(yīng)深色模式使用的色值和主題扔字。
所有的資源限定符定義以及添加的順序(例如-night
必須在-xhdpi
之前)可查看應(yīng)用資源概覽中的配置限定符名稱表囊嘉。
深色模式判斷&設(shè)置
判斷當(dāng)前是否深色模式
Configuration.uiMode 有三種NIGHT的模式
-
UI_MODE_NIGHT_NO 表示當(dāng)前使用的是
notnight
模式資源 -
UI_MODE_NIGHT_YES 表示當(dāng)前使用的是
night
模式資源 - UI_MODE_NIGHT_UNDEFINED 表示當(dāng)前沒有設(shè)置模式
可以通過以下的代碼來判斷當(dāng)前是否處于深色模式:
/**
* 判斷當(dāng)前是否深色模式
*
* @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: 對于一些從網(wǎng)絡(luò)接口服務(wù)獲取的需要對深色模式區(qū)分的色值或者圖片革为,可以使用上述的判斷來獲取對應(yīng)的資源哗伯。
判斷當(dāng)前深色模式場景
通過AppCompatDelegate.getDefaultNightMode()可以獲取五種深色模式場景:
- MODE_NIGHT_AUTO_BATTERY 低電量模式自動開啟深色模式
- MODE_NIGHT_FOLLOW_SYSTEM 跟隨系統(tǒng)開啟和關(guān)閉深色模式(默認(rèn))
-
MODE_NIGHT_NO 強制使用
notnight
資源,表示非深色模式 -
MODE_NIGHT_YES 強制使用
night
資源 - MODE_NIGHT_UNSPECIFIED 配合 setLocalNightMode(int) 使用篷角,表示由Activity通過AppCompactActivity.getDelegate()來單獨設(shè)置頁面的深色模式,不設(shè)置全局模式
模式設(shè)置
深色模式設(shè)置可以從三個層級設(shè)置系任,分別是系統(tǒng)層恳蹲、Applcation層以及Activity層虐块。底層的設(shè)置會覆蓋上層的設(shè)置,例如系統(tǒng)設(shè)置了深色模式嘉蕾,但是Application設(shè)置了淺色模式贺奠,那么應(yīng)用會顯示淺色主題。
系統(tǒng)層是指系統(tǒng)設(shè)置中错忱,根據(jù)不同產(chǎn)商的手機儡率,可以在設(shè)置->顯示中修改系統(tǒng)為深色模式。
Application層通過AppCompatDelegate.setDefaultNightMode()
設(shè)置深色模式以清。
Activity層通過getDelegate().setLocalNightMode()設(shè)置深色模式儿普。
當(dāng)深色模式改變時,Activity會重建掷倔,如果不希望Activity重建眉孩,可以在AndroidManifest.xml
中對對應(yīng)的Activity設(shè)置android:configChanges="uiMode"
,不過設(shè)置之后頁面的顏色改變需要Activity在中通過監(jiān)聽onConfigurationChanged
來動態(tài)改變勒葱。
通過AppCompatDelegate.setDefaultNightMode(int)可以設(shè)置深色模式浪汪,源碼如下:
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;
}
}
從源碼可以看出設(shè)置 MODE_NIGHT_UNSPECIFIED 模式是不會生效的。
Tips:注意凛虽,深色模式變化會導(dǎo)致Activity重建死遭。
適配方案
自定義適配
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瓦阐,那么使用對應(yīng)的Bridge主題可以快速切換到Material Design蜗侈。
從上面的分析可以看出,DayNight就是在values以及values-night中分別定義了淺色和深色的主題睡蟋。如果我們的主題直接繼承DayNight主題踏幻,那么就不需要重復(fù)地聲明對應(yīng)的night
主題資源了。
如果我們想對深色模式主題添加自定義屬性戳杀,那么我們可以不繼承DayNight主題该面,并顯示地聲明主題對應(yīng)的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: 若需要動態(tài)修改主題要在調(diào)用inflate之前調(diào)用信卡,否則不會生效隔缀。
2. 色值
主題切換顏色
除了定義不同模式使用不同的主題,我們還可以對主題設(shè)置自定義的色值傍菇。在設(shè)置主題色值之前猾瘸,我們先了解一下Android主題的顏色系統(tǒng)。
- colorPrimary:主要品牌顏色,一般用于ActionBar背景
- colorPrimaryDark:默認(rèn)用于頂部狀態(tài)欄和底部導(dǎo)航欄
- colorPrimaryVariant:主要品牌顏色的可選顏色
- colorSecondary:第二品牌顏色
- colorSecondaryVariant:第二品牌顏色的可選顏色
- colorPrimarySurface:對應(yīng)Light主題指向colorPrimary牵触,Dark主題指向colorSurface
- colorOn[Primary, Secondary, Surface ...]淮悼,在Primary等這些背景的上面內(nèi)容的顏色,例如ActioBar上面的文字顏色
- colorAccent:默認(rèn)設(shè)置給colorControlActivated揽思,一般是主要品牌顏色的明亮版本補充
- colorControlNormal:圖標(biāo)和控制項的正常狀態(tài)顏色
- colorControlActivated:圖標(biāo)和控制項的選中顏色(例如Checked或者Switcher)
- colorControlHighlight:點擊高亮效果(ripple或者selector)
- colorButtonNormal:按鈕默認(rèn)狀態(tài)顏色
- colorSurface:cards, sheets, menus等控件的背景顏色
- colorBackground:頁面的背景顏色
- colorError:展示錯誤的顏色
- textColorPrimary:主要文字顏色
- textColorSecondary:可選文字顏色
Tips: 當(dāng)某個屬性同時可以通過 ?attr/xxx
或者?android:attr/xxx
獲取時袜腥,最好使用?attr/xxx
,因為?android:attr/xxx
是通過系統(tǒng)獲取钉汗,而?attr/xxx
是通過靜態(tài)庫類似于AppCompat 或者 Material Design Component引入的羹令。使用非系統(tǒng)版本的屬性可以提高平臺通用性。
如果需要自定義主題顏色损痰,我們可以對顏色分別定義notnight
和night
兩份福侈,放在values
以及values-night
資源文件夾中,并在自定義主題時徐钠,傳入給對應(yīng)的顏色屬性癌刽。例如:
? 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>
這樣這個文字就會在深色模式中展示為黑底白字显拜,在非深色模式中展示為白底黑字。
動態(tài)設(shè)置顏色
如果需要代碼設(shè)置顏色爹袁,如果色值已經(jīng)設(shè)置過notnight
和night
兩份远荠,那么直接設(shè)置顏色就可以得到深色模式變色效果。
auto_color_text.setTextColor(ContextCompat.getColor(this, R.color.color_text_0))
如果色值是從服務(wù)接口獲取失息,那么可以使用上述深色模式的判斷設(shè)置譬淳。
auto_color_text.setTextColor(if (isNightMode()) {
Color.parseColor(darkColorFromNetwork)
} else {
Color.parseColor(colorFromNetwork)
})
3. 圖片&動畫
普通圖片&Gif圖片
將圖片分為明亮模式和深色模式兩份,分別放置在drawable-night-xxx
以及drawable-xxx
文件夾中盹兢,并在view中直接使用即可邻梆,當(dāng)深色模式切換時,會使用對應(yīng)深色模式的資源绎秒。如下圖所示:
<ImageView android:src="@drawable/round_fingerprint" />
Vector圖片
在Vector資源定義時浦妄,通過指定畫筆顏色來實現(xiàn)對深色模式的適配,例如:
<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
已經(jīng)分別定義好了notnight
和night
的色值剂娄。
Tips: 為了節(jié)省圖片占用的安裝包大小,我們可以盡量對圖標(biāo)都使用使用vector類型資源玄呛,具體可以在UI設(shè)計稿中導(dǎo)出SVG格式圖片阅懦,然后在Andorid Studio中導(dǎo)入,具體操作步驟如下:
Lottie
對于Lottie動畫徘铝,我們可以使用Lottie的Dynamic Properties特性來針對深色模式進行顏色變化耳胎。例如我們有以下兩個動畫惯吕,左邊是由顏色填充的機器人,右邊是由描邊生成的正在播放動畫场晶,我們可以調(diào)用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ài)改變顏色,例如我們抽取左右手臂以及機器小人身上的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")
// 設(shè)置右手臂顏色
lottie_android_animate.addValueCallback(rightArm, LottieProperty.COLOR) {
ContextCompat.getColor(this, R.color.color_main_1)
}
// 設(shè)置左手臂顏色
lottie_android_animate.addValueCallback(shirt, LottieProperty.COLOR) {
ContextCompat.getColor(this, R.color.color_light)
}
// 設(shè)置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
都已經(jīng)定義過深色模式和明亮模式的色值,因此在深色模式切換時揭北,Lottie動畫的這個機器小人的左右手臂和T恤顏色會隨著深色模式切換而變化扳炬。
同樣的對于播放動畫,我們也可以設(shè)置描邊顏色搔体,來達到深色模式切換的效果恨樟。
網(wǎng)絡(luò)獲取圖片
對于網(wǎng)絡(luò)獲取的圖片,可以讓服務(wù)接口分別給出明亮模式和深色模式兩套素材疚俱,然后根據(jù)上述的深色模式判斷來進行切換
Glide.with(this)
.load(if(isNightMode() nightImageUrl else imageUrl))
.into(imgView)
Force Dark
看到這里可能會有人有疑問劝术,對于大型的項目而言,里面已經(jīng)hardcore了很多的顏色值呆奕,并且很多圖片都沒有設(shè)計成深色模式的养晋,那做深色模式適配是不是一個不可能完成的任務(wù)呢?答案是否定的梁钾。對于大型項目而言绳泉,除了對所有的顏色和圖片定義night
資源的自定義適配方法外,我們還可以對使用Light
風(fēng)格主題的頁面進行進行強制深色模式轉(zhuǎn)換姆泻。
我們可以分別對主題和View設(shè)置強制深色模式零酪。對于主題,在Light
主題中設(shè)置android:forceDarkAllowed
拇勃,例如:
<style name="LightAppTheme" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge">
<!-- ... -->
<item name="android:forceDarkAllowed">true</item>
</style>
對于View四苇,設(shè)置View.setForceDarkAllowed(boolean)或者xml來設(shè)置是否支持Force Dark,默認(rèn)值是true方咆。
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:forceDarkAllowed="false"/>
這里需要注意的是月腋,F(xiàn)orce Dark的設(shè)置有以下幾個規(guī)則:
- 要強制深色模式生效必須開啟硬件加速(默認(rèn)開啟)
- 主題設(shè)置的Force Dark僅對
Light
的主題有效,對非Light
的主題不管是設(shè)置android:forceDarkAllowed
為true
或者設(shè)置View.setForceDarkAllowed(true)
都是無效的峻呛。 - 父節(jié)點設(shè)置了不支持Force Dark罗售,那么子節(jié)點再設(shè)置支持Force Dark無效。例如主題設(shè)置了
android:forceDarkAllowed
為false
钩述,則View設(shè)置View.setForceDarkAllowed(true)
無效寨躁。同樣的,如果View本身設(shè)置了支持Force Dark牙勘,但是其父layout設(shè)置了不支持职恳,那么該View不會執(zhí)行Force Dark - 子節(jié)點設(shè)置不支持Force Dark不受父節(jié)點設(shè)置支持Force Dark影響所禀。例如View設(shè)置了支持Force Dark,但是其子Layout設(shè)置了不支持放钦,那么子Layout也不會執(zhí)行Force Dark色徘。
Tips:一個比較容易記的規(guī)則就是不支持Force Dark優(yōu)先,View 的 Force Dark設(shè)置一般會設(shè)置成 false操禀,用于排除某些已經(jīng)適配了深色模式的 View褂策。
下面我們從源碼出發(fā)來理解Force Dark的這些行為,以及看看系統(tǒng)是怎么實現(xiàn)Force Dark的颓屑。
Tips:善用 https://cs.android.com/ 源碼搜索網(wǎng)站可以方便查看系統(tǒng)源碼斤寂。
1. 主題
從主題設(shè)置的forceDarkAllowed
入手查找,可以找到
frameworks/base/core/java/android/view/ViewRootImpl.java
private void updateForceDarkMode() {
if (mAttachInfo.mThreadedRenderer == null) return;
// 判斷當(dāng)前是否深色模式
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
// 如果當(dāng)前是深色模式
if (useAutoDark) {
// 獲取Force Dark的系統(tǒng)默認(rèn)值
boolean forceDarkAllowedDefault =
SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
// 判斷主題是否淺色主題 并且 判斷主題設(shè)置的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()
方法中調(diào)用的揪惦,因此可以得到第一個結(jié)論:強制深色模式只在硬件加速下生效遍搞。由于userAutoDark
變量會判斷當(dāng)前主題是否為淺色,因此可以得到第二個結(jié)論:強制深色模式只在淺色主題下生效器腋。直到這一步的調(diào)用鏈如下:
mAttachInfo.mThreadedRenderer
為ThreadRenderer
溪猿,繼承自HardwareRenderer
,指定了接下來的渲染操作由RanderThread執(zhí)行纫塌。繼續(xù)跟蹤setForceDark()
方法:
frameworks/base/graphics/java/android/graphics/HardwareRenderer.java
public boolean setForceDark(boolean enable) {
// 如果強制深色模式變化
if (mForceDark != enable) {
mForceDark = enable;
// 調(diào)用native層設(shè)置強制深色模式邏輯
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動態(tài)注冊
{ "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);
// 調(diào)用RenderProxy的setForceDark方法
proxy->setForceDark(enable);
}
frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
void RenderProxy::setForceDark(bool enable) {
// 調(diào)用CanvasContext的setForceDark方法
mRenderThread.queue().post([this, enable]() { mContext->setForceDark(enable); });
}
frameworks/base/libs/hwui/renderthread/CanvasContext.h
// Force Dark的默認(rèn)值是false
bool mUseForceDark = false;
// 設(shè)置mUseForceDark標(biāo)志
void setForceDark(bool enable) { mUseForceDark = enable; }
bool useForceDark() {
return mUseForceDark;
}
接著查找調(diào)用userForceDark()
方法的地方
frameworks/base/libs/hwui/TreeInfo.cpp
TreeInfo::TreeInfo(TraversalMode mode, renderthread::CanvasContext& canvasContext)
: mode(mode)
, prepareTextures(mode == MODE_FULL)
, canvasContext(canvasContext)
// 設(shè)置disableForceDark變量
, disableForceDark(canvasContext.useForceDark() ? 0 : 1)
, screenSize(canvasContext.getNextFrameSize()) {}
} // namespace android::uirenderer
frameworks/base/libs/hwui/TreeInfo.h
class TreeInfo {
public:
// ...
int disableForceDark;
// ...
};
到了這里,可以看出护戳,當(dāng)設(shè)置了Force Dark之后翎冲,最終會設(shè)置到TreeInfo
類中的disableForceDark
變量,如果沒有設(shè)置主題的Force Dark媳荒,那么根據(jù)false的默認(rèn)值抗悍,disableForceDark
變量會別設(shè)置成1,如果設(shè)置了使用強制深色模式钳枕,那么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);
}
// 如果當(dāng)前View不允許被ForceDark鱼炒,那么info.disableForceDark值+1
if (!mProperties.getAllowForceDark()) {
info.disableForceDark++;
}
// ...
// 同步正在處理的Render Node的Display List衔沼,實現(xiàn)具體深色的邏輯
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) {
// 遞歸調(diào)用子節(jié)點的prepareTreeImpl。
// 遞歸調(diào)用之前昔瞧,若父節(jié)點不允許強制深色模式指蚁,disableForceDark已經(jīng)不為0,
// 子節(jié)點再設(shè)置允許強制深色模式不會使得disableForceDark的值減少自晰,
// 因此有第三個規(guī)則:父節(jié)點設(shè)置了不允許深色模式凝化,子節(jié)點再設(shè)置允許深色模式無效。
// 同樣的酬荞,遞歸調(diào)用之前搓劫,若父節(jié)點允許深色模式瞧哟,disableForceDark為0,
// 子節(jié)點再設(shè)置不允許強制深色模式枪向,則disableForceDark值還是會++勤揩,不為0
// 因此有第四個規(guī)則:子節(jié)點設(shè)置不允許強制深色模式不受父節(jié)點設(shè)置允許強制深色模式影響。
child->prepareTreeImpl(observer, info, functorsNeedLayer);
});
if (isDirty) {
damageSelf(info);
}
}
pushLayerUpdate(info);
// 遞歸結(jié)束后將之前設(shè)置過+1的值做回退-1恢復(fù)操作秘蛔,避免影響其他兄弟結(jié)點的深色模式值判斷
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 {
// 設(shè)置WebViewSyncData的applyForceDark
.applyForceDark = info && !info->disableForceDark
};
mDisplayList->syncContents(syncData);
// 強制執(zhí)行深色模式執(zhí)行
handleForceDark(info);
}
}
void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
if (CC_LIKELY(!info || info->disableForceDark)) {
// 如果disableForceDark不為0陨亡,關(guān)閉強制深色模式,則直接返回
return;
}
auto usage = usageHint();
const auto& children = mDisplayList->mChildNodes;
// 如果有文字表示是前景策略
if (mDisplayList->hasText()) {
usage = UsageHint::Foreground;
}
if (usage == UsageHint::Unknown) {
// 如果子節(jié)點大于1或者第一個子節(jié)點不是背景深员,那么設(shè)置為背景策略
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);
}
}
// 根據(jù)前景還是背景策略對顏色進行提亮或者加深
mDisplayList->mDisplayList.applyColorTransform(
usage == UsageHint::Background ? ColorTransform::Dark : ColorTransform::Light);
}
Tips:View的繪制會根據(jù)VSYNC信號数苫,將UI線程的Display List樹同步到Render線程的Display List樹,并通過生產(chǎn)者消費者模式將layout信息放置到SurfaceFlinger中辨液,并最后交給Haredware Composer進行合成繪制。具體View渲染邏輯見參考章節(jié)的15~19文章列表箱残。
frameworks/base/libs/hwui/RecordingCanvas.cpp
void DisplayListData::applyColorTransform(ColorTransform transform) {
// 使用transform作為參數(shù)執(zhí)行color_transform_fns函數(shù)組
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滔迈,并調(diào)用對應(yīng)類型的colorTransformForOp函數(shù)
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[] = {
// 相當(dāng)于 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被辑,最終會調(diào)用到這里
// opRaw對應(yīng)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
這里進行具體的顏色轉(zhuǎn)換邏輯,我們首先關(guān)注非Bitmap繪制的顏色轉(zhuǎn)換
// 非Bitmap繪制顏色模式轉(zhuǎn)換
bool transformPaint(ColorTransform transform, SkPaint* paint) {
applyColorTransform(transform, *paint);
return true;
}
// 非Bitmap繪制顏色模式轉(zhuǎn)換
static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
if (transform == ColorTransform::None) return;
// 具體繪制顏色轉(zhuǎn)換邏輯
SkColor newColor = transformColor(transform, paint.getColor());
// 將畫筆顏色修改為轉(zhuǎn)換后的顏色
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++) {
// 對漸變色顏色進行轉(zhuǎn)換
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顏色進行轉(zhuǎn)換
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色彩模式轉(zhuǎn)換成Lab色彩模式
Lab lab = sRGBToLab(color);
// 對亮度L維度取反
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL > lab.L) {
// 若取反后亮度變亮盼理,則替換原來亮度
lab.L = invertedL;
// 重新轉(zhuǎn)換為sRGB模式
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}
// 后景色變暗
static SkColor makeDark(SkColor color) {
// 將sRGB色彩模式轉(zhuǎn)換成Lab色彩模式
Lab lab = sRGBToLab(color);
// 對亮度L維度取反
float invertedL = std::min(110 - lab.L, 100.0f);
if (invertedL < lab.L) {
// 若取反后亮度變暗谈山,則替換原來亮度
lab.L = invertedL;
// 重新轉(zhuǎn)換為sRGB模式
return LabToSRGB(lab, SkColorGetA(color));
} else {
return color;
}
}
從代碼中可以看出,深色模式應(yīng)用之后宏怔,通過對sRGB色彩空間轉(zhuǎn)換Lab色彩空間奏路,并對表示亮度的維度L進行取反,并判斷取反后前景色是不是更亮臊诊,后景色是不是更暗鸽粉,若是的話就替換為原來的L,并再重新轉(zhuǎn)換為sRGB色彩空間抓艳,從而實現(xiàn)反色的效果触机。
我們再來看對圖片的強制深色模式處理:
// Bitmap繪制顏色模式轉(zhuǎn)換
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;
// 設(shè)置skia反轉(zhuǎn)亮度的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玷或,并根據(jù)palette的明暗再疊加一層白色或者黑色
color = paint->getColorFilter()->filterColor(color);
// 根據(jù)將顏色轉(zhuǎn)換為HSV空間儡首,并返回是圖片的亮度是亮還是暗
return paletteForColorHSV(color);
}
從代碼中可以看出,對于Bitmap類型的繪制偏友,先判斷原來繪制Bitmap的明暗度蔬胯,如果原來繪制的圖像較為明亮但是需要變暗,或者原來繪制的圖像較為暗需要變明亮约谈,則設(shè)置一個明亮度轉(zhuǎn)換的filter到畫筆paint中笔宿。
至此犁钟,對于主題級別的強制深色轉(zhuǎn)換原理已經(jīng)非常清晰。總結(jié)一下泼橘,就是需要對前景色變亮和背景色變暗涝动,然后對于非Bitmap類型明暗變化采用的是將色值轉(zhuǎn)換為Lab顏色空間進行明亮度轉(zhuǎn)換,對于Bitmap類型的明暗變化采取設(shè)置亮度轉(zhuǎn)換的filter進行炬灭。
2. View
無論是設(shè)置View的xml的android:forceDarkAllowed
屬性醋粟,還是調(diào)用View.setForceDarkAllowed()
最后還是調(diào)用到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調(diào)用到android_view_RenderNode_setAllowForceDark
Navtive方法中重归。
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
變量被設(shè)置到RenderProperties.h
中
frameworks/base/libs/hwui/RenderProperties.h
/*
* Data structure that holds the properties for a RenderNode
*/
class ANDROID_API RenderProperties {
public:
// ...
// 設(shè)置View是否允許強制深色模式
bool setAllowForceDark(bool allow) {
return RP_SET(mPrimitiveFields.mAllowForceDark, allow);
}
// 獲取View是否允許強制深色模式
bool getAllowForceDark() const {
return mPrimitiveFields.mAllowForceDark;
}
// ...
private:
// Rendering properties
struct PrimitiveFields {
// ...
// 默認(rèn)值為true
bool mAllowForceDark = true;
// ...
} mPrimitiveFields;
我們回頭看下上面分析過的RenderNode.cpp
的prepareTreeImpl
流程
frameworks/base/libs/hwui/RenderNode.cpp
// 經(jīng)過了簡化處理的prepareTreeImpl邏輯
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info) {
// 如果當(dāng)前View不允許被ForceDark米愿,那么info.disableForceDark值+1
if (!mProperties.getAllowForceDark()) {
info.disableForceDark++;
}
// 同步正在處理的Render Node的Display List,實現(xiàn)具體深色的邏輯
pushStagingDisplayListChanges(observer, info);
mDisplayList->prepareListAndChildren([](RenderNode* child, TreeObserver& observer, TreeInfo& info) {
// 遞歸調(diào)用子節(jié)點的prepareTreeImpl鼻吮。
// 遞歸調(diào)用之前育苟,若父節(jié)點不允許強制深色模式,disableForceDark已經(jīng)不為0椎木,
// 子節(jié)點再設(shè)置允許強制深色模式不會使得disableForceDark的值減少违柏,
// 因此有第三個規(guī)則:父節(jié)點設(shè)置了不允許深色模式,子節(jié)點再設(shè)置允許深色模式無效香椎。
// 同樣的漱竖,遞歸調(diào)用之前,若父節(jié)點允許深色模式畜伐,disableForceDark為0馍惹,
// 子節(jié)點再設(shè)置不允許強制深色模式,則disableForceDark值還是會++玛界,不為0
// 因此有第四個規(guī)則:子節(jié)點設(shè)置不允許強制深色模式不受父節(jié)點設(shè)置允許強制深色模式影響万矾。
child->prepareTreeImpl(observer, info);
});
// 遞歸結(jié)束后將之前設(shè)置過+1的值做回退-1恢復(fù)操作,避免影響其他兄弟結(jié)點的深色模式值判斷
if (!mProperties.getAllowForceDark()) {
info.disableForceDark--;
}
}
可以看出脚仔,設(shè)置View的forceDarkAllowed
最終會設(shè)置到當(dāng)前RenderNode
的mProperties.allowForceDark
屬性中勤众,并在RenderNode
遍歷的過程中影響深色模式的執(zhí)行。
我們可以以下面的偽代碼來更直觀地了解深色模式執(zhí)行的流程:
// 深色模式渲染偽代碼
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--;
}
}
至此鲤脏,我們分析完所有強制深色模式的原理们颜。總結(jié)一下,主題默認(rèn)不會強制深色猎醇,若主題設(shè)置了強制深色窥突,則遍歷View樹對其節(jié)點進行強制深色轉(zhuǎn)換。碰到某個View不希望被強制深色硫嘶,則包括它和它的所有子節(jié)點都不會被強制深色阻问。
總結(jié)
到這里,我們了解了可以通過設(shè)置-night
資源以及判斷當(dāng)前顏色模式來自定義切換主題沦疾、色值称近、圖片和動畫的顏色第队,也從源代碼角度了解Force Dark的原理和生效規(guī)則。
Demo
上述提到的代碼可以到這個Github項目下載
參考
- 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應(yīng)用程序UI硬件加速渲染環(huán)境初始化過程分析
- Android應(yīng)用程序UI硬件加速渲染的Display List構(gòu)建過程分析
- Android應(yīng)用程序UI硬件加速渲染的Display List渲染過程分析
- Drawn out: how Android renders (Google I/O '18)
- 深入理解Android的渲染機制
- SKIA api
- Android Code Search