【Android 10】深色主題

我們一直以來使用的操作系統都是以淺色主題爲主的,這種主題模式在白天或者是光線充足的情況下使用起來沒有任何問題,可是在夜晚燈光關閉的情況下使用就會顯得非常刺眼。

於是,許多應用程序爲了能夠讓用戶在光線昏暗的環境下更加舒適地使用,會在應用內部提供一個一鍵切換夜間模式的按鈕。當用戶開啓了夜間模式,就會將應用程序的整體色調都調整成更加適合於夜間瀏覽的顏色。

不過,這種由應用程序自發實現夜間模式的方式很難做到全局統一,即有些應用可能支持夜間模式,有些應用卻不支持。而且重複操作的問題也很讓人頭疼,比如說我在一個應用中開啓了夜間模式,在另外一個應用中還需要再開啓一次,關閉夜間模式也需要進行同樣重複的操作。

因此,一直以來都有強烈的呼聲,希望Android能夠在系統層面支持夜間模式功能。終於在Android 10.0系統中,Google引入了深色主題這一特性,從而讓夜間模式正式成爲了官方支持的功能。

或許你會有些疑惑,這種看上去並沒有太多技術難度的功能,爲什麼Android直到10.0系統中才進行支持呢?這是因爲僅僅操作系統自身支持深色主題是沒有用的,還得讓所有的應用程序都能夠支持才行,而這從來都不是一件容易的事情。

爲此,我們以後開發的應用程序都應該儘量按照Android系統的要求對深色主題進行支持,不然當用戶開啓了深色主題之後,只有你的應用還使用的是淺色主題的話,就會顯得格格不入。

除了讓眼部在夜間使用時更加舒適之外,深色主題還可以減少電量消耗,從而延長手機續航,是一項非常有用的功能。那麼接下來,我們就開始學習如何才能讓應用程序支持深色主題功能。

首先,Android 10.0及以上系統的手機,都可以在Settings -> Display -> Dark theme中對深色主題進行開啓和關閉。開啓深色主題後,系統的界面風格包括一些內置的應用程序都會變成深色主題的色調,如下圖所示。

這裏我準備使用在第12章中編寫的MaterialTest項目來作爲示例,看看如何才能讓它更加完美地適配深色主題模式。(示例下載地址見鏈接隨書下載部分 https://www.ituring.com.cn/book/2744)。

首先看一下MaterialTest項目的初始運行效果。
 

接下來我們開始學習如何深色主題模式進行適配。

最簡單的一種適配方式就是使用Force Dark,它是一種能讓應用程序快速適配深色主題,並且幾乎不用編寫額外代碼的方式。Force Dark的工作原理是系統會分析淺色主題應用下的每一層View,並且在這些View繪製到屏幕之前,自動將它們的顏色轉換成更加適合深色主題的顏色。注意,只有原本使用淺色主題的應用才能使用這種方式,如果你的應用原本使用的就是深色主題,Force Dark將不會起作用。

這裏我們嘗試對MaterialTest項目使用Force Dark轉換來進行舉例。啓用Force Dark功能需要藉助android:forceDarkAllowed屬性,不過這個屬性是從API 29,也就是Android 10.0系統開始纔有的,之前的系統無法指定這個屬性。因此,我們得進行一些系統差異型編程才行。

右擊res目錄 -> New -> Directory,創建一個values-v29目錄,然後右擊values-v29目錄 -> New -> Values resource file,創建一個styles.xml文件。接着對這個文件進行編寫,代碼如下所示:
 

<resources>
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:forceDarkAllowed">true</item>
    </style>
</resources>

除了android:forceDarkAllowed屬性之外,其他的內容都是從之前的styles.xml文件中複製過來的。這裏給AppTheme主題增加了android:forceDarkAllowed屬性並設置爲true,說明現在我們是允許系統使用Force Dark將應用強制轉換成深色主題的。另外,values-v29目錄是隻有Android 10.0及以上的系統纔會去讀取的,因此這是一種系統差異型編程的實現方式。

現在重新運行MaterialTest項目,效果如下圖所示。

可以看到,雖然整體的界面風格好像確實變成了深色主題的模式,可是卻並不怎麼美觀,尤其是卡片式佈局的效果,經過Force Dark之後已經完全看不出來了。

Force Dark就是這樣一種簡單粗暴的轉換方式,並且它的轉換效果通常是不盡如人意的。因此,這裏我並不推薦你使用這種自動化的方式來實現深色主題,而是應該使用更加傳統的實現方式——手動實現。

是的,要想實現最佳的深色主題效果,不要指望有什麼神奇魔法能夠一鍵完成,而是應該針對每一個界面都進行淺色和深色兩種主題的界面設計。這聽上去好像有點複雜,不過我們仍然有一些好用的技巧能讓這個過程變得簡單。

在第12章中我們曾經學習過,AppCompat庫內置的主題恰好主要分爲淺色主題和深色主題兩類,比如MaterialTest項目中目前使用的Theme.AppCompat.Light.NoActionBar就是淺色主題,而Theme.AppCompat.NoActionBar就是深色主題。選用不同的主題,在控件的默認顏色等方面會有完全不同的效果。

下面我們動手來嘗試一下吧。首先刪除values-v29目錄及其目錄下的內容,然後修改values/styles.xml中的代碼,如下所示:
 

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
    …
</resources>

可以看到,這裏我們將AppTheme的parent主題指定成了Theme.AppCompat.DayNight.NoActionBar ,這是一種DayNight主題。因此,在普通情況下MaterialTest項目仍然會使用淺色主題,和之前並沒有什麼區別,但是一旦用戶在系統設置中開啓了深色主題,MaterialTest項目就會自動使用相應的深色主題。

現在我們就可以重新運行一下程序,看看使用DayNight主題之後,MaterialTest項目默認的界面效果是什麼樣的,如下圖所示。

很明顯,現在的界面比之前使用Force Dark轉換後的界面要好看很多,至少卡片式佈局的效果得到了保留。

然而,雖然現在界面中的主要內容都已經自動切換成了深色主題,但是你會發現標題欄和懸浮按鈕仍然保持着和淺色主題時一樣的顏色。這是因爲標題欄以及懸浮按鈕使用的是我們定義在colors.xml中的幾種顏色值,代碼如下所示:
 

<resources>
    <color name="colorPrimary">#008577</color>
    <color name="colorPrimaryDark">#00574B</color>
    <color name="colorAccent">#D81B60</color>
</resources>

這樣的話,在普通情況下,系統仍然會讀取values/colors.xml文件中的顏色值,而一旦用戶開啓了深色主題,系統就會去讀取values-night/colors.xml文件中的顏色值了。

現在重新運行一下程序,效果如下圖所示。

這種深色主題的效果,在夜間使用的時候明顯會好上很多,對不對?

雖說使用主題差異型的編程方式幾乎可以幫你解決所有的適配問題,但是在DayNight主題下,我們最好還是儘量減少通過硬編碼的方式來指定控件的顏色,而是應該更多地使用能夠根據當前主題自動切換顏色的主題屬性。比如說黑色的文字通常應該襯托在白色的背景下,反之白色的文字通常應該襯托在黑色的背景下,那麼此時我們就可以使用主題屬性來指定背景以及文字的顏色,示例寫法如下:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/colorBackground">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Hello world"
        android:textSize="40sp"
        android:textColor="?android:attr/textColorPrimary" />

</FrameLayout>

這些主題屬性會自動根據系統當前的主題模式選擇最合適的顏色值呈現給用戶,效果如下圖所示。

另外,或許你還會有一些特殊的需求,比如要在淺色主題和深色主題下分別執行不同的代碼邏輯。對此Android也是支持的,你可以使用如下代碼在任何時候判斷當前系統是否是深色主題:

fun isDarkTheme(context: Context): Boolean {
    val flag = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
    return flag == Configuration.UI_MODE_NIGHT_YES
}

調用isDarkTheme()方法,判斷當前系統是淺色主題還是深色主題,然後根據返回值執行不同的代碼邏輯即可。

由於Kotlin取消了按位運算符的寫法,改成了使用英文關鍵字,因此上述代碼中的and關鍵字其實就對應了Java中的&運算符,而Kotlin中的or關鍵字對應了Java中的|運算符,xor關鍵字對應了Java中的^運算符,非常好理解。

我個人認爲,在絕大多數情況下,讓應用程序跟隨系統的設置來決定使用淺色主題還是深色主題是最合適的一種做法。然而如果你一定想要脫離系統設置,讓自己的應用程序獨立控制使用淺色主題還是深色主題,Android對此也是支持的,只要使用AppCompatDelegate.setDefaultNightMode()方法即可。

setDefaultNightMode()方法接收一個mode參數,用於控制當前應用程序的夜間模式。mode參數主要有以下值可供選擇:

MODE_NIGHT_FOLLOW_SYSTEM:默認模式,表示讓當前應用程序跟隨系統設置來決定使用淺色主題還是深色主題。

MODE_NIGHT_YES:脫離系統設置,強制讓當前應用程序使用深色主題。

MODE_NIGHT_NO:脫離系統設置,強制讓當前應用程序使用淺色主題。

MODE_NIGHT_AUTO_BATTERY:根據手機的電池狀態來決定使用淺色主題還是深色主題,如果開啓了節點模式,則使用深色主題。

在MaterialTest當中,我們只需要使用如下代碼就可以實現淺色主題和深色主題動態切換的功能:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        fab.setOnClickListener { view ->
            if (isDarkTheme(this)) {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            } else {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            }
        }
    }
	
    ...
	
}

運行之後的效果如下圖所示。

需要注意的是,當調用setDefaultNightMode()方法併成功切換主題時,應用程序中所有處於started狀態的Activity都會被重新創建(不在started狀態的Activity則會在恢復started狀態時再重新創建,關於started狀態的解釋可以參見《第一行代碼 第3版》的第13章,Lifecycles部分)。這很好理解,因爲Activity不重新創建Activity怎麼切換主題呢?

可是如果你當前所處的界面不想被重新創建,比如一個正在播放視頻的界面。這個時候可以在Activity的configChanges屬性當中配置uiMode來讓當前Activity避免被重新創建,如下所示:

<activity
    android:name=".MainActivity"
    android:configChanges="uiMode" />

 現在當應用程序的主題發生變化時,MainActivity並不會重新創建,而是會觸發onConfigurationChanged()方法的回調,你可以在回調當中手動做一些邏輯處理。

override fun onConfigurationChanged(newConfig: Configuration) {
    val currentNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK
    when (currentNightMode) {
        Configuration.UI_MODE_NIGHT_NO -> {} // 夜間模式未啓用,使用淺色主題
        Configuration.UI_MODE_NIGHT_YES -> {} // 夜間模式啓用,使用深色主題
    }
}

如果什麼都不做的話,當前的Activity就好像並沒有切換主題一樣,界面上也不會有任何變化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章