博文出處:對于Android日夜間模式實現(xiàn)的探討番官,歡迎大家關(guān)注我的博客原杂,謝謝!
0x0001
======
關(guān)于 Android 的日間/夜間模式切換相信大家在平時使用 APP 的過程中都遇到過瓢对,比如知乎靠胜、簡書中就有相關(guān)的模式切換。實現(xiàn)日間/夜間模式切換的方案也有許多種饮醇,趁著今天有空來講一下日間/夜間模式切換的幾種實現(xiàn)方案它抱,也可以做一個橫向的對比來看看哪種方案最好。
在本篇文章中給出了三種實現(xiàn)日間/夜間模式切換的方案:
- 使用 setTheme 的方法讓 Activity 重新設(shè)置主題朴艰;
- 設(shè)置 Android Support Library 中的 UiMode 來支持日間/夜間模式的切換观蓄;
- 通過資源 id 映射,回調(diào)自定義 ThemeChangeListener 接口來處理日間/夜間模式的切換祠墅。
三種方案綜合起來可能導(dǎo)致文章的篇幅過長侮穿,請耐心閱讀。
0x0002
使用 setTheme 方法
我們先來看看使用 setTheme 方法來實現(xiàn)日間/夜間模式切換的方案毁嗦。這種方案的思路很簡單亲茅,就是在用戶選擇夜間模式時,Activity 設(shè)置成夜間模式的主題狗准,之后再讓 Activity 調(diào)用 recreate() 方法重新創(chuàng)建一遍就行了克锣。
那就動手吧,在 colors.xml 中定義兩組顏色腔长,分別表示日間和夜間的主題色:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</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
屬性是我們自定義的屬性,用來表示背景色:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="mainBackground" format="color|reference"></attr>
</resources>
接下來就是看一下布局 activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/mainBackground"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.yuqirong.themedemo.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>
在 <RelativeLayout>
的 android:background
屬性中捞附,我們使用 "?attr/mainBackground"
來表示巾乳,這樣就代表著 RelativeLayout
的背景色會去引用在主題中事先定義好的 mainBackground
屬性的值。這樣就實現(xiàn)了日間/夜間模式切換的換色了鸟召。
最后就是 MainActivity 的代碼:
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");
}
}
在 MainActivity 中有幾點要注意一下:
調(diào)用
recreate()
方法后 Activity 的生命周期會調(diào)用onSaveInstanceState(Bundle outState)
來備份相關(guān)的數(shù)據(jù)想鹰,之后也會調(diào)用onRestoreInstanceState(Bundle savedInstanceState)
來還原相關(guān)的數(shù)據(jù),因此我們把theme
的值保存進去药版,以便 Activity 重新創(chuàng)建后使用辑舷。我們在
onCreate(Bundle savedInstanceState)
方法中還原得到了theme
值后,setTheme()
方法一定要在setContentView()
方法之前調(diào)用槽片,否則的話就看不到效果了何缓。recreate()
方法是在 API 11 中添加進來的,所以在 Android 2.X 中使用會拋異常还栓。
貼完上面的代碼之后碌廓,我們來看一下該方案實現(xiàn)的效果圖:
使用 Android Support Library 中的 UiMode 方法
使用 UiMode 的方法也很簡單,我們需要把 colors.xml 定義為日間/夜間兩種。之后根據(jù)不同的模式會去選擇不同的 colors.xml 敷存。在 Activity 調(diào)用 recreate() 之后,就實現(xiàn)了切換日/夜間模式的功能痴脾。
說了這么多纪挎,直接上代碼期贫。下面是 values/colors.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="textColor">#FF000000</color>
<color name="backgroundColor">#FFFFFF</color>
</resources>
除了 values/colors.xml 之外,我們還要創(chuàng)建一個 values-night/colors.xml 文件异袄,用來設(shè)置夜間模式的顏色通砍,其中 <color>
的 name 必須要和 values/colors.xml 中的相對應(yīng):
<?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>
在 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>
activity_main.xml 布局的內(nèi)容和上面 setTheme() 方法中的相差無幾,這里就不貼出來了烤蜕。之后的事情就變得很簡單了封孙,在 MyApplication 中先選擇一個默認的 Mode :
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 默認設(shè)置為日間模式
AppCompatDelegate.setDefaultNightMode(
AppCompatDelegate.MODE_NIGHT_NO);
}
}
要注意的是,這里的 Mode 有四種類型可以選擇:
- MODE_NIGHT_NO: 使用亮色(light)主題讽营,不使用夜間模式虎忌;
- MODE_NIGHT_YES:使用暗色(dark)主題,使用夜間模式橱鹏;
- MODE_NIGHT_AUTO:根據(jù)當(dāng)前時間自動切換 亮色(light)/暗色(dark)主題呐籽;
- MODE_NIGHT_FOLLOW_SYSTEM(默認選項):設(shè)置為跟隨系統(tǒng),通常為 MODE_NIGHT_NO
當(dāng)用戶點擊按鈕切換日/夜間時蚀瘸,重新去設(shè)置相應(yīng)的 Mode :
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);
// 同樣需要調(diào)用recreate方法使之生效
recreate();
}
});
}
}
我們來看一下 UiMode 方案實現(xiàn)的效果圖:
就前兩種方法而言狡蝶,配置比較簡單,最后的實現(xiàn)效果也都基本上是一樣的贮勃。但是缺點就是需要調(diào)用 recreate()
使之生效贪惹。而讓 Activity 重新創(chuàng)建就必須涉及到一些狀態(tài)的保存。這就增加了一些難度寂嘉。所以奏瞬,我們一起來看看第三種解決方法。
通過資源 id 映射泉孩,回調(diào)接口
第三種方法的思路就是根據(jù)設(shè)置的主題去動態(tài)地獲取資源 id 的映射硼端,然后使用回調(diào)接口的方式讓 UI 去設(shè)置相關(guān)的屬性值。我們在這里先規(guī)定一下:夜間模式的資源在命名上都要加上后綴 “_night” 寓搬,比如日間模式的背景色命名為 color_background 珍昨,那么相對應(yīng)的夜間模式的背景資源就要命名為 color_background_night 。好了句喷,下面就是我們的 Demo 所需要用到的 colors.xml :
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimary_night">#3b3b3b</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorPrimaryDark_night">#383838</color>
<color name="colorAccent">#FF4081</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>
可以看到每一項 color 都會有對應(yīng)的 “_night” 與之匹配镣典。
看到這里,肯定有人會問唾琼,為什么要設(shè)置對應(yīng)的 “_night” 兄春?到底是通過什么方式來設(shè)置日/夜間模式的呢?下面就由 ThemeManager 來為你解答:
public class ThemeManager {
// 默認是日間模式
private static ThemeMode mThemeMode = ThemeMode.DAY;
// 主題模式監(jiān)聽器
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
}
/**
* 設(shè)置主題模式
*
* @param themeMode
*/
public static void setThemeMode(ThemeMode themeMode) {
if (mThemeMode != themeMode) {
mThemeMode = themeMode;
if (mThemeChangeListenerList.size() > 0) {
for (OnThemeChangeListener listener : mThemeChangeListenerList) {
listener.onThemeChanged();
}
}
}
}
/**
* 根據(jù)傳入的日間模式的resId得到相應(yīng)主題的resId,注意:必須是日間模式的resId
*
* @param dayResId 日間模式的resId
* @return 相應(yīng)主題的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 {
//如果緩存中沒有再根據(jù)資源id去動態(tài)獲取
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;
}
/**
* 主題模式切換監(jiān)聽器
*/
public interface OnThemeChangeListener {
/**
* 主題切換時回調(diào)
*/
void onThemeChanged();
}
}
上面 ThemeManager 的代碼基本上都有注釋蒸辆,想要看懂并不困難。其中最核心的就是 getCurrentThemeRes
方法了析既。在這里解釋一下 getCurrentThemeRes
的邏輯躬贡。參數(shù)中的 dayResId 是日間模式的資源id,如果當(dāng)前主題是日間模式的話眼坏,就直接返回 dayResId 拂玻。反之當(dāng)前主題為夜間模式的話,先根據(jù) dayResId 得到資源名稱和資源類型宰译。比如現(xiàn)在有一個資源為 R.color.colorPrimary 檐蚜,那么資源名稱就是 colorPrimary ,資源類型就是 color 沿侈。然后根據(jù)資源類型和資源名稱去獲取緩存闯第。如果沒有緩存,那么就要動態(tài)獲取資源了缀拭。這里使用方法的是
context.getResources().getIdentifier(String name, String defType, String defPackage)
-
name
參數(shù)就是資源名稱咳短,不過要注意的是這里的資源名稱還要加上后綴 “_night” ,也就是上面在 colors.xml 中定義的名稱蛛淋; -
defType
參數(shù)就是資源的類型了咙好。比如 color,drawable等褐荷; -
defPackage
就是資源文件的包名勾效,也就是當(dāng)前 APP 的包名。
有了上面的這個方法叛甫,就可以通過 R.color.colorPrimary 資源找到對應(yīng)的 R.color.colorPrimary_night 資源了葵第。最后還要把找到的夜間模式資源加入到緩存中。這樣的話以后就直接去緩存中讀取合溺,而不用再次去動態(tài)查找資源 id 了卒密。
ThemeManager 中剩下的代碼應(yīng)該都是比較簡單的,相信大家都可以看得懂了棠赛。
現(xiàn)在我們來看看 MainActivity 的代碼:
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)));
// 設(shè)置標題欄顏色
if(supportActionBar != null){
supportActionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary))));
}
// 設(shè)置狀態(tài)欄顏色
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 中實現(xiàn)了 OnThemeChangeListener 接口哮奇,這樣就可以在主題改變的時候執(zhí)行回調(diào)方法膛腐。然后在 initTheme()
中去重新設(shè)置 UI 的相關(guān)顏色屬性值。還有別忘了要在 onDestroy()
中移除 ThemeChangeListener 鼎俘。
最后就來看看第三種方法的效果吧:
也許有人會說和前兩種方法的效果沒什么差異啊哲身,但是仔細看就會發(fā)現(xiàn)前面兩種方法在切換模式的瞬間會有短暫黑屏現(xiàn)象存在,而第三種方法沒有贸伐。這是因為前兩種方法都要調(diào)用 recreate()
勘天。而第三種方法不需要 Activity 重新創(chuàng)建,使用回調(diào)的方法來實現(xiàn)捉邢。
0x0003
到了這里脯丝,按照套路應(yīng)該是要總結(jié)的時候了。那么就根據(jù)上面給的三種方法來一個簡單的對比吧:
setTheme 方法:可以配置多套主題伏伐,比較容易上手宠进。除了日/夜間模式之外,還可以有其他五顏六色的主題藐翎。但是需要調(diào)用 recreate() 材蹬,切換瞬間會有黑屏閃現(xiàn)的現(xiàn)象;
UiMode 方法:優(yōu)點就是 Android Support Library 中已經(jīng)支持吝镣,簡單規(guī)范堤器。但是也需要調(diào)用 recreate() ,存在黑屏閃現(xiàn)的現(xiàn)象末贾;
動態(tài)獲取資源 id 吼旧,回調(diào)接口:該方法使用起來比前兩個方法復(fù)雜,另外在回調(diào)的方法中需要設(shè)置每一項 UI 相關(guān)的屬性值未舟。但是不需要調(diào)用 recreate() 圈暗,沒有黑屏閃現(xiàn)的現(xiàn)象。
三種方法整體的對比就如上所示了裕膀。當(dāng)然除了上面的三種方法實現(xiàn)日/夜間模式切換之外员串,還有比如動態(tài)換膚等也都可以實現(xiàn)。方法有很多種昼扛,重要的是要根據(jù)自身情況選擇合適的方法去實現(xiàn)寸齐。在下面我會給出其他幾種實現(xiàn)日/夜間模式切換方法的鏈接,可以參考一下抄谐。
好了渺鹦,到了說再見的時候了。
Goodbye !