Android中夜間模式的三種實現方式
參考:https://www.jianshu.com/p/f3aaed57fa15
在本篇文章中給出了三種實現日間/夜間模式切換的方案:
- 使用 setTheme 的方法讓 Activity 重新設定主題;
- 設定 Android Support Library 中的 UiMode 來支援日間/夜間模式的切換;
- 通過資源 id 對映,回撥自定義 ThemeChangeListener 介面來處理日間/夜間模式的切換。
一、使用 setTheme 方法
我們先來看看使用 setTheme 方法來實現日間/夜間模式切換的方案。這種方案的思路很簡單,就是在使用者選擇夜間模式時,Activity 設定成夜間模式的主題,之後再讓 Activity 呼叫 recreate() 方法重新建立一遍就行了。
第一步:在 colors.xml 中定義兩組顏色,分別表示日間和夜間的主題色。
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- 日間主題色 --> <color name="colorPrimary">#008577</color> <color name="colorPrimaryDark">#00574B</color> <color name="colorAccent">#D81B60</color> <!-- 夜間主題色 --> <color name="nightColorPrimary">#3b3b3b</color> <color name="nightColorPrimaryDark">#383838</color> <color name="nightColorAccent">#a72b55</color> </resources>
第二步:在 styles.xml 中定義兩組主題,也就是日間主題和夜間主題。
<resources> <!-- Base application theme. 日間主題 --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <item name="android:textColor">@android:color/black</item> <item name="mainBackground">@android:color/white</item> </style> <!-- 夜間主題 --> <style name="NightAppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/nightColorPrimary</item> <item name="colorPrimaryDark">@color/nightColorPrimaryDark</item> <item name="colorAccent">@color/nightColorAccent</item> <item name="android:textColor">@android:color/white</item> <item name="mainBackground">@color/nightColorPrimaryDark</item> </style> </resources>
第三步:在主題中的 mainBackground
屬性是我們自定義的屬性,用來表示背景色。所以我們需要在values裡建立一個attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="mainBackground" format="color|reference"></attr>
</resources>
第四步:在佈局中呼叫mainBackground屬性。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="?attr/mainBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="切換日/夜間模式" />
</LinearLayout>
在 <
LinearLayout>
的 android:background
屬性中,我們使用 "?attr/mainBackground"
來表示,這樣就代表著 LinearLayout 的背景色會去引用在主題中事先定義好的 mainBackground
屬性的值。這樣就實現了日間/夜間模式切換的換色了。
第五步:在Activity中的配置。
public class MainActivity extends AppCompatActivity {
// 預設是日間模式
private int theme = R.style.AppTheme;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 判斷是否有主題儲存
if(savedInstanceState != null){
theme = savedInstanceState.getInt("theme");
setTheme(theme);
}
setContentView(R.layout.activity_main);
Button btn_theme = (Button) findViewById(R.id.btn_theme);
btn_theme.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
theme = (theme == R.style.AppTheme) ? R.style.NightAppTheme : R.style.AppTheme;
MainActivity.this.recreate();
}
});
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("theme", theme);
}
@Override protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
theme = savedInstanceState.getInt("theme");
}
}
在 Activity 中有幾點要注意一下:
-
呼叫
recreate()
方法後 Activity 的生命週期會呼叫onSaveInstanceState(Bundle outState)
來備份相關的資料,之後也會呼叫onRestoreInstanceState(Bundle savedInstanceState)
來還原相關的資料,因此我們把theme
的值儲存進去,以便 Activity 重新建立後使用。 -
我們在
onCreate(Bundle savedInstanceState)
方法中還原得到了theme
值後,setTheme()
方法一定要在setContentView()
方法之前呼叫,否則的話就看不到效果了。 -
recreate()
方法是在 API 11 中新增進來的,所以在 Android 2.X 中使用會拋異常。
二、使用 Android Support Library 中的 UiMode 方法
使用 UiMode 的方法也很簡單,我們需要把 colors.xml 定義為日間/夜間兩種。之後根據不同的模式會去選擇不同的 colors.xml 。在 Activity 呼叫 recreate() 之後,就實現了切換日/夜間模式的功能。
第一步:在values/colors.xml配置。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
<color name="textColor">#FF000000</color>
<color name="backgroundColor">#FFFFFF</color>
</resources>
第二步:除了 values/colors.xml 之外,我們還要建立一個 values-night/colors.xml 檔案,用來設定夜間模式的顏色,其中 <color>
的 name 必須要和 values/colors.xml 中的相對應。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3b3b3b</color>
<color name="colorPrimaryDark">#383838</color>
<color name="colorAccent">#a72b55</color>
<color name="textColor">#FFFFFF</color>
<color name="backgroundColor">#3b3b3b</color>
</resources>
第三步:在values裡建立一個attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="mainBackground" format="color|reference"></attr>
</resources>
第四步:在 styles.xml 中去引用我們在 colors.xml 中定義好的顏色。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:textColor">@color/textColor</item>
<item name="mainBackground">@color/backgroundColor</item>
</style>
</resources>
第五步:在佈局中呼叫mainBackground屬性。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="?attr/mainBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="切換日/夜間模式" />
</LinearLayout>
第六步:建立一個Application先選擇一個預設的 Mode。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 預設設定為日間模式
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_NO);
}
}
要注意的是,這裡的 Mode 有四種類型可以選擇:
- MODE_NIGHT_NO: 使用亮色(light)主題,不使用夜間模式;
- MODE_NIGHT_YES:使用暗色(dark)主題,使用夜間模式;
- MODE_NIGHT_AUTO:根據當前時間自動切換 亮色(light)/暗色(dark)主題;
- MODE_NIGHT_FOLLOW_SYSTEM(預設選項):設定為跟隨系統,通常為 MODE_NIGHT_NO
第七步 :在Activity中控制呼叫。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_theme = (Button) findViewById(R.id.btn_theme);
btn_theme.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
getDelegate().setLocalNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO ? AppCompatDelegate.MODE_NIGHT_YES :
AppCompatDelegate.MODE_NIGHT_NO); // 同樣需要呼叫recreate方法使之生效 recreate();
}
});
}
}
三、通過資源 id 對映,回撥介面
第三種方法的思路就是根據設定的主題去動態地獲取資源 id 的對映,然後使用回撥介面的方式讓 UI 去設定相關的屬性值。我們在這裡先規定一下:夜間模式的資源在命名上都要加上字尾 “_night” ,比如日間模式的背景色命名為 color_background ,那麼相對應的夜間模式的背景資源就要命名為 color_background_night 。
第一步:在colors.xml中配置。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimary_night">#3b3b3b</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorPrimaryDark_night">#383838</color>
<color name="colorAccent">#D81B60</color>
<color name="colorAccent_night">#a72b55</color>
<color name="textColor">#FF000000</color>
<color name="textColor_night">#FFFFFF</color>
<color name="backgroundColor">#FFFFFF</color>
<color name="backgroundColor_night">#3b3b3b</color>
</resources>
第二步:建立ThemeManager類。
public class ThemeManager {
// 預設是日間模式
private static ThemeMode mThemeMode = ThemeMode.DAY;
// 主題模式監聽器
private static List<OnThemeChangeListener> mThemeChangeListenerList = new LinkedList<>();
// 夜間資源的快取,key : 資源型別, 值<key:資源名稱, value:int值>
private static HashMap<String, HashMap<String, Integer>> sCachedNightResrouces = new HashMap<>();
// 夜間模式資源的字尾,比如日件模式資源名為:R.color.activity_bg, 那麼夜間模式就為 :R.color.activity_bg_night
private static final String RESOURCE_SUFFIX = "_night";
/**
* 主題模式,分為日間模式和夜間模式
*/
public enum ThemeMode {
DAY, NIGHT
}
/**
* 設定主題模式
*
* @param themeMode
*/
public static void setThemeMode(ThemeMode themeMode) {
if (mThemeMode != themeMode) {
mThemeMode = themeMode;
if (mThemeChangeListenerList.size() > 0) {
for (OnThemeChangeListener listener : mThemeChangeListenerList) {
listener.onThemeChanged();
}
}
}
}
/**
* 根據傳入的日間模式的resId得到相應主題的resId,注意:必須是日間模式的resId
* * *
* @param dayResId 日間模式的resId
* @return 相應主題的resId,若為日間模式,則得到dayResId;反之夜間模式得到nightResId
*/
public static int getCurrentThemeRes(Context context, int dayResId) {
if (getThemeMode() == ThemeMode.DAY) {
return dayResId;
}
// 資源名
String entryName = context.getResources().getResourceEntryName(dayResId);
// 資源型別
String typeName = context.getResources().getResourceTypeName(dayResId);
HashMap<String, Integer> cachedRes = sCachedNightResrouces.get(typeName);
// 先從快取中去取,如果有直接返回該id
if (cachedRes == null) {
cachedRes = new HashMap<>();
}
Integer resId = cachedRes.get(entryName + RESOURCE_SUFFIX);
if (resId != null && resId != 0) {
return resId;
} else {
//如果快取中沒有再根據資源id去動態獲取
try {
// 通過資源名,資源型別,包名得到資源int值
int nightResId = context.getResources().getIdentifier(entryName + RESOURCE_SUFFIX, typeName, context.getPackageName());
// 放入快取中
cachedRes.put(entryName + RESOURCE_SUFFIX, nightResId);
sCachedNightResrouces.put(typeName, cachedRes);
return nightResId;
} catch (Resources.NotFoundException e) {
e.printStackTrace();
}
}
return 0;
}
/**
* 註冊ThemeChangeListener
*
* @param listener
*/
public static void registerThemeChangeListener(OnThemeChangeListener listener) {
if (!mThemeChangeListenerList.contains(listener)) {
mThemeChangeListenerList.add(listener);
}
}
/**
* 反註冊ThemeChangeListener
*
* @param listener
*/
public static void unregisterThemeChangeListener(OnThemeChangeListener listener) {
if (mThemeChangeListenerList.contains(listener)) {
mThemeChangeListenerList.remove(listener);
}
}
/**
* 得到主題模式
*
* @return
*/
public static ThemeMode getThemeMode() {
return mThemeMode;
}
/**
* 主題模式切換監聽器
*/
public interface OnThemeChangeListener {
/**
* 主題切換時回撥
*/
void onThemeChanged();
}
}
上面 ThemeManager 的程式碼基本上都有註釋,想要看懂並不困難。其中最核心的就是 getCurrentThemeRes
方法了。在這裡解釋一下 getCurrentThemeRes
的邏輯。引數中的 dayResId 是日間模式的資源id,如果當前主題是日間模式的話,就直接返回 dayResId 。反之當前主題為夜間模式的話,先根據 dayResId 得到資源名稱和資源型別。比如現在有一個資源為 R.color.colorPrimary ,那麼資源名稱就是 colorPrimary ,資源型別就是 color 。然後根據資源型別和資源名稱去獲取快取。如果沒有快取,那麼就要動態獲取資源了。這裡使用方法的是
context.getResources().getIdentifier(String name, String defType, String defPackage)
name
引數就是資源名稱,不過要注意的是這裡的資源名稱還要加上字尾 “_night” ,也就是上面在 colors.xml 中定義的名稱;defType
引數就是資源的型別了。比如 color,drawable等;defPackage
就是資原始檔的包名,也就是當前 APP 的包名。
有了上面的這個方法,就可以通過 R.color.colorPrimary 資源找到對應的 R.color.colorPrimary_night 資源了。最後還要把找到的夜間模式資源加入到快取中。這樣的話以後就直接去快取中讀取,而不用再次去動態查詢資源 id 了。
第三步:佈局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="切換日/夜間模式" />
<TextView
android:id="@+id/tv"
android:layout_below="@id/btn_theme"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="通過setTheme()的方法" />
</RelativeLayout>
第三步:在Activity中控制顯示。
public class MainActivity extends AppCompatActivity implements ThemeManager.OnThemeChangeListener {
private TextView tv;
private Button btn_theme;
private RelativeLayout relativeLayout;
private ActionBar supportActionBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ThemeManager.registerThemeChangeListener(this);
supportActionBar = getSupportActionBar();
btn_theme = (Button) findViewById(R.id.btn_theme);
relativeLayout = (RelativeLayout) findViewById(R.id.relativeLayout);
tv = (TextView) findViewById(R.id.tv);
btn_theme.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
ThemeManager.setThemeMode(ThemeManager.getThemeMode() == ThemeManager.ThemeMode.DAY ? ThemeManager.ThemeMode.NIGHT : ThemeManager.ThemeMode.DAY);
}
});
}
public void initTheme() {
tv.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
btn_theme.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
relativeLayout.setBackgroundColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.backgroundColor)));
// 設定標題欄顏色
if(supportActionBar != null){
supportActionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary))));
}
// 設定狀態列顏色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow();
window.setStatusBarColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary)));
}
}
@Override
public void onThemeChanged() {
initTheme();
}
@Override
protected void onDestroy() {
super.onDestroy();
ThemeManager.unregisterThemeChangeListener(this);
}
}
在 MainActivity 中實現了 OnThemeChangeListener 介面,這樣就可以在主題改變的時候執行回撥方法。然後在 initTheme()
中去重新設定 UI 的相關顏色屬性值。還有別忘了要在 onDestroy()
中移除 ThemeChangeListener 。
四、總結
-
setTheme 方法:可以配置多套主題,比較容易上手。除了日/夜間模式之外,還可以有其他五顏六色的主題。但是需要呼叫 recreate() ,切換瞬間會有黑屏閃現的現象;
-
UiMode 方法:優點就是 Android Support Library 中已經支援,簡單規範。但是也需要呼叫 recreate() ,存在黑屏閃現的現象;
-
動態獲取資源 id ,回撥介面:該方法使用起來比前兩個方法複雜,另外在回撥的方法中需要設定每一項 UI 相關的屬性值。但是不需要呼叫 recreate() ,沒有黑屏閃現的現象。