Android實(shí)現(xiàn)日夜間模式的深入理解
在本篇文章中給出了三種實(shí)現(xiàn)日間/夜間模式切換的方案,三種方案綜合起來(lái)可能導(dǎo)致文章的篇幅過(guò)長(zhǎng),請(qǐng)耐心閱讀。
1、使用 setTheme
的方法讓 Activity
重新設(shè)置主題;
2、設(shè)置 Android Support Library
中的 UiMode
來(lái)支持日間/夜間模式的切換;
3、通過(guò)資源 id 映射,回調(diào)自定義 ThemeChangeListener
接口來(lái)處理日間/夜間模式的切換。
一、使用 setTheme 方法
我們先來(lái)看看使用 setTheme
方法來(lái)實(shí)現(xiàn)日間/夜間模式切換的方案。這種方案的思路很簡(jiǎn)單,就是在用戶選擇夜間模式時(shí),Activity 設(shè)置成夜間模式的主題,之后再讓 Activity
調(diào)用 recreate()
方法重新創(chuàng)建一遍就行了。
那就動(dò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
屬性是我們自定義的屬性,用來(lái)表示背景色:
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="mainBackground" format="color|reference"></attr> </resources>
接下來(lái)就是看一下布局 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="通過(guò)setTheme()的方法" /> </RelativeLayout>
在 <RelativeLayout> 的 android:background
屬性中,我們使用 "?attr/mainBackground" 來(lái)表示,這樣就代表著 RelativeLayout
的背景色會(huì)去引用在主題中事先定義好的 mainBackground
屬性的值。這樣就實(shí)現(xiàn)了日間/夜間模式切換的換色了。
最后就是 MainActivity
的代碼:
public class MainActivity extends AppCompatActivity { // 默認(rèn)是日間模式 private int theme = R.style.AppTheme; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 判斷是否有主題存儲(chǔ) 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ǎn)要注意一下:
1、調(diào)用 recreate()
方法后 Activity 的生命周期會(huì)調(diào)用 onSaveInstanceState(Bundle outState)
來(lái)備份相關(guān)的數(shù)據(jù),之后也會(huì)調(diào)用 onRestoreInstanceState(Bundle savedInstanceState)
來(lái)還原相關(guān)的數(shù)據(jù),因此我們把 theme
的值保存進(jìn)去,以便 Activity 重新創(chuàng)建后使用。
2、我們?cè)?onCreate(Bundle savedInstanceState)
方法中還原得到了 theme
值后,setTheme()
方法一定要在 setContentView()
方法之前調(diào)用,否則的話就看不到效果了。
3、recreate()
方法是在 API 11 中添加進(jìn)來(lái)的,所以在 Android 2.X 中使用會(huì)拋異常。
貼完上面的代碼之后,我們來(lái)看一下該方案實(shí)現(xiàn)的效果圖:
二、使用 Android Support Library 中的 UiMode 方法
使用 UiMode 的方法也很簡(jiǎn)單,我們需要把 colors.xml 定義為日間/夜間兩種。之后根據(jù)不同的模式會(huì)去選擇不同的 colors.xml 。在 Activity 調(diào)用 recreate()
之后,就實(shí)現(xiàn)了切換日/夜間模式的功能。
說(shuō)了這么多,直接上代碼。下面是 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 之外,我們還要?jiǎng)?chuàng)建一個(gè) values-night/colors.xml 文件,用來(lái)設(shè)置夜間模式的顏色,其中 <color> 的 name 必須要和 values/colors.xml 中的相對(duì)應(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 中去引用我們?cè)?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()
方法中的相差無(wú)幾,這里就不貼出來(lái)了。之后的事情就變得很簡(jiǎn)單了,在 MyApplication 中先選擇一個(gè)默認(rèn)的 Mode :
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); // 默認(rèn)設(shè)置為日間模式 AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.MODE_NIGHT_NO); } }
要注意的是,這里的 Mode 有四種類型可以選擇:
1、MODE_NIGHT_NO: 使用亮色(light)主題,不使用夜間模式;
2、MODE_NIGHT_YES:使用暗色(dark)主題,使用夜間模式;
3、MODE_NIGHT_AUTO:根據(jù)當(dāng)前時(shí)間自動(dòng)切換 亮色(light)/暗色(dark)主題;
4、MODE_NIGHT_FOLLOW_SYSTEM(默認(rèn)選項(xiàng)):設(shè)置為跟隨系統(tǒng),通常為 MODE_NIGHT_NO
當(dāng)用戶點(diǎn)擊按鈕切換日/夜間時(shí),重新去設(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(); } }); } }
我們來(lái)看一下 UiMode
方案實(shí)現(xiàn)的效果圖:
就前兩種方法而言,配置比較簡(jiǎn)單,最后的實(shí)現(xiàn)效果也都基本上是一樣的。但是缺點(diǎn)就是需要調(diào)用 recreate()
使之生效。而讓 Activity 重新創(chuàng)建就必須涉及到一些狀態(tài)的保存。這就增加了一些難度。所以,我們一起來(lái)看看第三種解決方法。
通過(guò)資源 id 映射,回調(diào)接口
第三種方法的思路就是根據(jù)設(shè)置的主題去動(dòng)態(tài)地獲取資源 id 的映射,然后使用回調(diào)接口的方式讓 UI 去設(shè)置相關(guān)的屬性值。我們?cè)谶@里先規(guī)定一下:夜間模式的資源在命名上都要加上后綴 “_night” ,比如日間模式的背景色命名為 color_background ,那么相對(duì)應(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>
可以看到每一項(xiàng) color 都會(huì)有對(duì)應(yīng)的 “_night” 與之匹配。
看到這里,肯定有人會(huì)問(wèn),為什么要設(shè)置對(duì)應(yīng)的 “_night” ?到底是通過(guò)什么方式來(lái)設(shè)置日/夜間模式的呢?下面就由 ThemeManager 來(lái)為你解答:
public class ThemeManager { // 默認(rèn)是日間模式 private static ThemeMode mThemeMode = ThemeMode.DAY; // 主題模式監(jiān)聽(tīng)器 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 { //如果緩存中沒(méi)有再根據(jù)資源id去動(dòng)態(tài)獲取 try { // 通過(guò)資源名,資源類型,包名得到資源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; } /** * 注冊(cè)ThemeChangeListener * * @param listener */ public static void registerThemeChangeListener(OnThemeChangeListener listener) { if (!mThemeChangeListenerList.contains(listener)) { mThemeChangeListenerList.add(listener); } } /** * 反注冊(cè)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)聽(tīng)器 */ public interface OnThemeChangeListener { /** * 主題切換時(shí)回調(diào) */ void onThemeChanged(); } }
上面 ThemeManager 的代碼基本上都有注釋,想要看懂并不困難。其中最核心的就是 getCurrentThemeRes
方法了。在這里解釋一下 getCurrentThemeRes
的邏輯。參數(shù)中的 dayResId 是日間模式的資源id,如果當(dāng)前主題是日間模式的話,就直接返回 dayResId 。反之當(dāng)前主題為夜間模式的話,先根據(jù) dayResId 得到資源名稱和資源類型。比如現(xiàn)在有一個(gè)資源為 R.color.colorPrimary
,那么資源名稱就是 colorPrimary ,資源類型就是 color 。然后根據(jù)資源類型和資源名稱去獲取緩存。如果沒(méi)有緩存,那么就要?jiǎng)討B(tài)獲取資源了。這里使用方法的是
context.getResources().getIdentifier(String name, String defType, String defPackage)
name 參數(shù)就是資源名稱,不過(guò)要注意的是這里的資源名稱還要加上后綴 “_night” ,也就是上面在 colors.xml 中定義的名稱;
defType 參數(shù)就是資源的類型了。比如 color,drawable等;
defPackage 就是資源文件的包名,也就是當(dāng)前 APP 的包名。
有了上面的這個(gè)方法,就可以通過(guò) R.color.colorPrimary
資源找到對(duì)應(yīng)的 R.color.colorPrimary_night
資源了。最后還要把找到的夜間模式資源加入到緩存中。這樣的話以后就直接去緩存中讀取,而不用再次去動(dòng)態(tài)查找資源 id 了。
ThemeManager 中剩下的代碼應(yīng)該都是比較簡(jiǎn)單的,相信大家都可以看得懂了。
現(xiàn)在我們來(lái)看看 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è)置標(biāo)題欄顏色 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 中實(shí)現(xiàn)了 OnThemeChangeListener
接口,這樣就可以在主題改變的時(shí)候執(zhí)行回調(diào)方法。然后在 initTheme()
中去重新設(shè)置 UI 的相關(guān)顏色屬性值。還有別忘了要在 onDestroy()
中移除 ThemeChangeListener 。
最后就來(lái)看看第三種方法的效果吧:
也許有人會(huì)說(shuō)和前兩種方法的效果沒(méi)什么差異啊,但是仔細(xì)看就會(huì)發(fā)現(xiàn)前面兩種方法在切換模式的瞬間會(huì)有短暫黑屏現(xiàn)象存在,而第三種方法沒(méi)有。這是因?yàn)榍皟煞N方法都要調(diào)用 recreate()
。而第三種方法不需要 Activity 重新創(chuàng)建,使用回調(diào)的方法來(lái)實(shí)現(xiàn)。
三個(gè)方法對(duì)比
到了這里,按照套路應(yīng)該是要總結(jié)的時(shí)候了。那么就根據(jù)上面給的三種方法來(lái)一個(gè)簡(jiǎn)單的對(duì)比吧:
setTheme
方法:可以配置多套主題,比較容易上手。除了日/夜間模式之外,還可以有其他五顏六色的主題。但是需要調(diào)用 recreate()
,切換瞬間會(huì)有黑屏閃現(xiàn)的現(xiàn)象;
UiMode
方法:優(yōu)點(diǎn)就是 Android Support Library 中已經(jīng)支持,簡(jiǎn)單規(guī)范。但是也需要調(diào)用 recreate()
,存在黑屏閃現(xiàn)的現(xiàn)象;
動(dòng)態(tài)獲取資源 id ,回調(diào)接口:該方法使用起來(lái)比前兩個(gè)方法復(fù)雜,另外在回調(diào)的方法中需要設(shè)置每一項(xiàng) UI 相關(guān)的屬性值。但是不需要調(diào)用 recreate()
,沒(méi)有黑屏閃現(xiàn)的現(xiàn)象。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望能對(duì)各位Android開(kāi)發(fā)者們有所幫助。
相關(guān)文章
RecylerView實(shí)現(xiàn)流布局StaggeredGridLayoutManager使用詳解
這篇文章主要為大家詳細(xì)介紹了RecylerView實(shí)現(xiàn)流布局StaggeredGridLayoutManager使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09Android的APK應(yīng)用簽名機(jī)制以及讀取簽名的方法
這篇文章主要介紹了Android的APK應(yīng)用簽名機(jī)制以及讀取簽名的方法,這里作者推薦使用Java自帶的API進(jìn)行APK簽名的讀取,需要的朋友可以參考下2016-02-02Android調(diào)試出現(xiàn)The selected device is incompatible問(wèn)題解決
這篇文章主要介紹了Android調(diào)試出現(xiàn)The selected device is incompatible問(wèn)題解決的相關(guān)資料,需要的朋友可以參考下2017-01-01Android實(shí)現(xiàn)可拖動(dòng)層疊卡片布局
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)可拖動(dòng)層疊卡片布局,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11android指定DatePickerDialog樣式并不顯示年的實(shí)現(xiàn)代碼
下面小編就為大家?guī)?lái)一篇android指定DatePickerDialog樣式并不顯示年的實(shí)現(xiàn)代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,祝大家游戲愉快哦2016-08-08在RecyclerView中實(shí)現(xiàn)button的跳轉(zhuǎn)功能
本次實(shí)驗(yàn)就是在RecyclerView中添加一個(gè)button控件并實(shí)現(xiàn)監(jiān)聽(tīng),使鼠標(biāo)點(diǎn)擊時(shí)可以跳轉(zhuǎn)到另外一個(gè)設(shè)計(jì)好的界面,對(duì)RecyclerView實(shí)現(xiàn)button跳轉(zhuǎn)功能感興趣的朋友一起看看吧2021-10-10如何用HMS Nearby Service給自己的App添加近距離數(shù)據(jù)傳輸功能
這篇文章主要介紹了如何用HMS Nearby Service給自己的App添加近距離數(shù)據(jù)傳輸功能,本文通過(guò)圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07