1. 程式人生 > >Android-夜間模式最佳實踐

Android-夜間模式最佳實踐

如何優雅地實現夜間模式?在Android應用普遍支援夜間模式的今天,本文作者馬俊同學,分析了業界主流的方案, 同時也通過自己的研究,發現了一個維護成本相對較小的方案,讓我們一起看看。

由於Android的設定中並沒有夜間模式的選項,對於喜歡睡前玩手機的使用者,只能簡單的調節手機螢幕亮度來改善體驗。目前越來越多的應用開始把夜間模式加到自家應用中,沒準不久google也會把這項功能新增到Android系統中吧。

業內關於夜間模式的實現,有兩種主流方案,各有其利弊,我較為推崇第三種方案:
1、通過切換theme來實現夜間模式。
2、通過資源id對映的方式來實現夜間模式。
3、通過修改uiMode來切換夜間模式。

值得一提的是,上面提到的幾種方案,都是資源內嵌在Apk中的方案,像新浪微博那種需要通過下載方式實現的夜間模式方案,網上有很多介紹,這裡不去討論。

下面簡要描述下幾種方案的實現原理:

一、通過切換theme來實現夜間模式

首先在attrs.xml中,為需要隨theme變化的內容定義屬性

<?xml version="1.0" encoding="utf-8"?>
<resources> <attr name="textColor" format="color|reference" /> <attr name="mainBackground"
format="color|reference" />
</resources>

其次在不同的theme中,對屬性設定不同的值,在styles.xml中定義theme如下

<?xml version="1.0" encoding="utf-8"?>
<resources> <!-- 預設 --> <style name="ThemeDefault" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="mainBackground">#ffffff
</item> <item name="textColor">#000000</item>
</style> <!-- 夜間 --> <style name="ThemeNight" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="mainBackground">#000000</item> <item name="textColor">#ffffff</item>
</style>
</resources>

在佈局檔案中使用對應的值,通過?attr/屬性名,來獲取不同theme對應的值。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android="@+id/main_screen"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="?attr/mainBackground">
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="改變Theme"
        android:onClick="changeTheme"
        android:textColor="?attr/textColor"/>
</LinearLayout>

在Activity中呼叫如下changeTheme方法,其中isNightMode為一個全域性變數用來標記當前是否為夜間模式,在設定完theme後,還需要呼叫restartActivity或者setContentView重新重新整理UI。

public void changeTheme() {
if (isNightMode) { setTheme(R.style.ThemeDefault); isNightMode = false; } else { setTheme(R.style.ThemeNight); isNightMode = true; } setContentView(R.layout.activity_main); }

到此即完成了一個夜間模式的簡單實現,包括Google自家在內的很多應用都是採用此種方式實現夜間模式的,這應該也是Android官方推薦的方式。

但這種方式有一些不足,規模較大的應用,需要隨theme變化的屬性會很多,都需要逐一定義,有點麻煩,另外一個缺點是要使得新theme生效,一般需要restartActivity來切換UI,會導致切換主題時介面閃爍。

不過也可以通過呼叫如下updateTheme方法,只更新需要更新的部分,規避閃爍問題,只是需要寫上一堆updateTheme方法。

private void updateTheme() {
    TypedValue typedValue = new TypedValue();
    Resources.Theme theme = getTheme();
    theme.resolveAttribute(R.attr.textColor, typedValue, true);
    findViewById(R.id.button).setBackgroundColor(typedValue.data);
    theme.resolveAttribute(R.attr.mainBackground, typedValue, true);
    findViewById(R.id.main_screen).setBackgroundColor(typedValue.data);
}

二、通過資源id對映的方式實現夜間模式

通過id獲取資源時,先將其轉換為夜間模式對應id,再通過Resources來獲取對應的資源。

public static Drawable getDrawable(Context context, int id) {
return context.getResources().getDrawable(getResId(id)); }

public
static int getResId(int defaultResId)

if
(!isNightMode()) {
return defaultResId; }
if (sResourceMap == null) {
buildResourceMap(); }
int themedResId = sResourceMap.get(defaultResId);
return themedResId == 0 ? defaultResId : themedResId; }

這裡是通過HashMap將白天模式的resId和夜間模式的resId來一一對應起來的。

private static void buildResourceMap() {
    sResourceMap = new SparseIntArray();
    sResourceMap.put(R.drawable.common_background, R.drawable.common_background_night);
    // ...
}

這個方案簡單粗暴,麻煩的地方和第一種方案一樣:每次新增資源都需要建立對映關係,重新整理UI的方式也與第一種方案類似,貌似今日頭條,網易新聞客戶端等主流新聞閱讀應用都是通過這種方式實現的夜間模式。

三、通過修改uiMode來切換夜間模式

首先將獲取資源的地方統一起來,使用Application對應的Resources,在Application的onCreate中呼叫ResourcesManager的init方法將其初始化。

public static void init(Context context) {
    sRes = context.getResources();
}

切換夜間模式時,通過更新uiMode來更新Resources的配置,系統會根據其uiMode讀取對應night下的資源,同時在res中給夜間模式的資源新增-night字尾,比如values-night,drawable-night。

public static void updateNightMode(boolean on) {
    DisplayMetrics dm = sRes.getDisplayMetrics();
    Configuration config = sRes.getConfiguration();
    config.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
    config.uiMode |= on ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;
    sRes.updateConfiguration(config, dm);
}

至於Android的資源讀取,我們可以參考老羅的部落格《Android應用程式資源的查詢過程》,分析看看資源是怎麼被精準找到的。這種方法相對前兩種的好處就是資源新增非常簡單清晰,但是UI上的更新還是無法做到非常順滑的切換。

我是怎麼找到第三種方案的?

在Android開發文件中搜索night發現如下,可以通過UiModeManager來實現

night: Night time

notnight: Day time

Added in API level 8.

This can change during the life of your application if night mode is left in auto mode (default), in which case the mode changes based on the time of day. You can enable or disable this mode using UiModeManager. See Handling Runtime Changes for information about how this affects your application during runtime.


不幸的是必須在駕駛模式下才有效,那是不是開啟駕駛模式再設定呢,實際上是不可行的,駕駛模式下系統UI有變動,這樣是不可取的。

/**
* Sets the night mode. Changes to the night mode are only effective when
* the car or desk mode is enabled on a device.
*
* The mode can be one of:
* {@link #MODE_NIGHT_NO}- sets the device into notnight
* mode.
* {@link #MODE_NIGHT_YES} - sets the device into night mode.
* {@link #MODE_NIGHT_AUTO} - automatic night/notnight switching
* depending on the location and certain other sensors.
*/
public void setNightMode(int mode)

從原始碼開始看起,UiModeManagerService.java的setNightMode方法中:

if (isDoingNightModeLocked() && mNightMode != mode) {
    Settings.Secure.putInt(getContext().getContentResolver(), Settings.Secure.UI_NIGHT_MODE, mode);
    mNightMode = mode;
    updateLocked(0, 0);
}

boolean isDoingNightModeLocked() {
return mCarModeEnabled || mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED; }

在 isDoingNightModeLocked中判斷了DockState和mCardMode的狀態,如果滿足條件實際上只修改了mNightMode的值,繼續跟蹤updateLocked方法,可以看到在updateConfigurationLocked中更新了Configuration的uiMode。

讓我們轉向Configuration的uiMode的描述:

/**
* Bit mask of the ui mode. Currently there are two fields:
*
The {@link #UI_MODE_TYPE_MASK} bits define the overall ui mode of the
* device. They may be one of {@link #UI_MODE_TYPE_UNDEFINED},
* {@link #UI_MODE_TYPE_NORMAL}, {@link #UI_MODE_TYPE_DESK},
* {@link #UI_MODE_TYPE_CAR}, {@link #UI_MODE_TYPE_TELEVISION},
* {@link #UI_MODE_TYPE_APPLIANCE}, or {@link #UI_MODE_TYPE_WATCH}.
*
*
The {@link #UI_MODE_NIGHT_MASK} defines whether the screen
* is in a special mode. They may be one of {@link #UI_MODE_NIGHT_UNDEFINED},
* {@link #UI_MODE_NIGHT_NO} or {@link #UI_MODE_NIGHT_YES}.
*/
public int uiMode;

uiMode為public可以直接設定,既然UiModeManager設定nightMode只改了Configuration的uiMode,那我們是不是可以直接改其uiMode呢?

實際上只需要上面一小段程式碼就可以實現了,但如果不去檢視UiModeManager的夜間模式的實現,不會想到只需要更新Configuration的uiMode就可以了。