Android實(shí)現(xiàn)日夜間模式的深入理解
在本篇文章中給出了三種實(shí)現(xiàn)日間/夜間模式切換的方案,三種方案綜合起來可能導(dǎo)致文章的篇幅過長,請耐心閱讀。
1、使用 setTheme
的方法讓 Activity
重新設(shè)置主題;
2、設(shè)置 Android Support Library
中的 UiMode
來支持日間/夜間模式的切換;
3、通過資源 id 映射,回調(diào)自定義 ThemeChangeListener
接口來處理日間/夜間模式的切換。
一、使用 setTheme 方法
我們先來看看使用 setTheme
方法來實(shí)現(xià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
屬性是我們自定義的屬性,用來表示背景色:
<?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
屬性的值。這樣就實(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); // 判斷是否有主題存儲 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 的生命周期會調(diào)用 onSaveInstanceState(Bundle outState)
來備份相關(guān)的數(shù)據(jù),之后也會調(diào)用 onRestoreInstanceState(Bundle savedInstanceState)
來還原相關(guān)的數(shù)據(jù),因此我們把 theme
的值保存進(jìn)去,以便 Activity 重新創(chuàng)建后使用。
2、我們在 onCreate(Bundle savedInstanceState)
方法中還原得到了 theme
值后,setTheme()
方法一定要在 setContentView()
方法之前調(diào)用,否則的話就看不到效果了。
3、recreate()
方法是在 API 11 中添加進(jìn)來的,所以在 Android 2.X 中使用會拋異常。
貼完上面的代碼之后,我們來看一下該方案實(shí)現(xiàn)的效果圖:
二、使用 Android Support Library 中的 UiMode 方法
使用 UiMode 的方法也很簡單,我們需要把 colors.xml 定義為日間/夜間兩種。之后根據(jù)不同的模式會去選擇不同的 colors.xml 。在 Activity 調(diào)用 recreate()
之后,就實(shí)現(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 之外,我們還要?jiǎng)?chuàng)建一個(gè) 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 中先選擇一個(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(); } }); } }
我們來看一下 UiMode
方案實(shí)現(xiàn)的效果圖:
就前兩種方法而言,配置比較簡單,最后的實(shí)現(xiàn)效果也都基本上是一樣的。但是缺點(diǎn)就是需要調(diào)用 recreate()
使之生效。而讓 Activity 重新創(chuàng)建就必須涉及到一些狀態(tài)的保存。這就增加了一些難度。所以,我們一起來看看第三種解決方法。
通過資源 id 映射,回調(diào)接口
第三種方法的思路就是根據(jù)設(shè)置的主題去動(dòng)態(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>
可以看到每一項(xiàng) color 都會有對應(yīng)的 “_night” 與之匹配。
看到這里,肯定有人會問,為什么要設(shè)置對應(yīng)的 “_night” ?到底是通過什么方式來設(shè)置日/夜間模式的呢?下面就由 ThemeManager 來為你解答:
public class ThemeManager { // 默認(rèn)是日間模式 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去動(dòng)態(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 { /** * 主題切換時(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ù)資源類型和資源名稱去獲取緩存。如果沒有緩存,那么就要?jiǎng)討B(tài)獲取資源了。這里使用方法的是
context.getResources().getIdentifier(String name, String defType, String defPackage)
name 參數(shù)就是資源名稱,不過要注意的是這里的資源名稱還要加上后綴 “_night” ,也就是上面在 colors.xml 中定義的名稱;
defType 參數(shù)就是資源的類型了。比如 color,drawable等;
defPackage 就是資源文件的包名,也就是當(dāng)前 APP 的包名。
有了上面的這個(gè)方法,就可以通過 R.color.colorPrimary
資源找到對應(yīng)的 R.color.colorPrimary_night
資源了。最后還要把找到的夜間模式資源加入到緩存中。這樣的話以后就直接去緩存中讀取,而不用再次去動(dòng)態(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è)置標(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 。
最后就來看看第三種方法的效果吧:
也許有人會說和前兩種方法的效果沒什么差異啊,但是仔細(xì)看就會發(fā)現(xiàn)前面兩種方法在切換模式的瞬間會有短暫黑屏現(xiàn)象存在,而第三種方法沒有。這是因?yàn)榍皟煞N方法都要調(diào)用 recreate()
。而第三種方法不需要 Activity 重新創(chuàng)建,使用回調(diào)的方法來實(shí)現(xiàn)。
三個(gè)方法對比
到了這里,按照套路應(yīng)該是要總結(jié)的時(shí)候了。那么就根據(jù)上面給的三種方法來一個(gè)簡單的對比吧:
setTheme
方法:可以配置多套主題,比較容易上手。除了日/夜間模式之外,還可以有其他五顏六色的主題。但是需要調(diào)用 recreate()
,切換瞬間會有黑屏閃現(xiàn)的現(xiàn)象;
UiMode
方法:優(yōu)點(diǎn)就是 Android Support Library 中已經(jīng)支持,簡單規(guī)范。但是也需要調(diào)用 recreate()
,存在黑屏閃現(xiàn)的現(xiàn)象;
動(dòng)態(tài)獲取資源 id ,回調(diào)接口:該方法使用起來比前兩個(gè)方法復(fù)雜,另外在回調(diào)的方法中需要設(shè)置每一項(xiàng) UI 相關(guān)的屬性值。但是不需要調(diào)用 recreate()
,沒有黑屏閃現(xiàn)的現(xiàn)象。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望能對各位Android開發(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問題解決
這篇文章主要介紹了Android調(diào)試出現(xiàn)The selected device is incompatible問題解決的相關(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ī)硪黄猘ndroid指定DatePickerDialog樣式并不顯示年的實(shí)現(xiàn)代碼。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,祝大家游戲愉快哦2016-08-08在RecyclerView中實(shí)現(xiàn)button的跳轉(zhuǎn)功能
本次實(shí)驗(yàn)就是在RecyclerView中添加一個(gè)button控件并實(shí)現(xiàn)監(jiān)聽,使鼠標(biāo)點(diǎn)擊時(shí)可以跳轉(zhuǎn)到另外一個(gè)設(shè)計(jì)好的界面,對RecyclerView實(shí)現(xiàn)button跳轉(zhuǎn)功能感興趣的朋友一起看看吧2021-10-10如何用HMS Nearby Service給自己的App添加近距離數(shù)據(jù)傳輸功能
這篇文章主要介紹了如何用HMS Nearby Service給自己的App添加近距離數(shù)據(jù)傳輸功能,本文通過圖文示例代碼相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07