簡潔優雅地實現夜間模式
前言
Android 6.0 Marshmallow 預覽版中曾經短暫出現過相關的夜間模式的功能,只是在正式版中被移除了,在Android 7.0 Nougat上,用戶們再次經歷了「得而復失」的遺憾,在開發者預覽版中,夜間模式和暗色模式先是開啟,然后有再次被移除。而在正式版中,夜間模式也沒有出現。但其實相關的代碼一直存在于系統中,只是默認沒有被開啟。如何開啟這項功能,可以參考少數派的這一篇文章, 幫你找回 Android 7.0 夜間模式的 2 款應用 。
不過,今天要介紹的主要內容并不是關于系統的夜間模式,而是如何給我們開發的APP添加夜間模式的功能。毫不夸張的說,夜間模式現在已經是閱讀類App的標配了。事實上,日間模式與夜間模式就是給APP定義并應用兩套不同顏色的主題。用戶可以自動或者手動的開啟。我們先看兩個我認為實現地很優雅的例子:知乎和推ter。
這兩個APP在切換的工程中,并沒有出現閃現黑屏的情況,切換也比較順滑。我們的目標就是利用Support Library實現同樣的效果。
實現
添加依賴
compile 'com.android.support:appcompat-v7:25.1.0'
由于Support Library在23.2.0的版本中才添加了Theme.AppCompat.DayNight主題,所以依賴的版本必須是高于23.2.0的,并且,這個特性支持的最低SDK版本為14,所以,需要兼容Android 4.0的設備,是不能使用這個特性的,在API Level 14以下的設備會默認使用亮色主題。不過現在4.0以下的設備應該比較少了吧,畢竟微信的minSdkVersion都設置為14了。
準備資源
-
讓我們自己的主題繼承并應用DayNight主題。
<style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar"> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> <!--customize your theme here--> </style>
-
新建夜間模式資源文件夾:在 res 目錄下新建 values-night 文件夾,然后在此目錄下新建 colors.xml 文件在夜間模式下的應用的資源。當然也可以根據需要新建 drawable-night , layout-night 等后綴為 -night 的夜間資源文件夾。
我的 values 和 values-night 目錄下的 colors.xml 的內容如下:
<?xml version="1.0" encoding="utf-8"?> <!--values-colors.xml--> <resources> <color name="colorPrimary">#009688</color> <color name="colorPrimaryDark">#00796B</color> <color name="colorAccent">#009688</color> <color name="textColorPrimary">#616161</color> <color name="viewBackground">@android:color/white</color> </resources>
<?xml version="1.0" encoding="utf-8"?> <!--values-night-colors.xml> <resources> <color name="colorPrimary">#35464e</color> <color name="colorPrimaryDark">#212a2f</color> <color name="colorAccent">#212a2f</color> <color name="textColorPrimary">#616161</color> <color name="viewBackground">#212a2f</color> </resources>
-
使Activity繼承自AppCompatActivity。
public class MainActivity extends AppCompatActivity { // code here @Override protected void onCreate(Bundle savedInstanceState) { } }
應用
靜態應用
在Application的繼承類下設置初始主題。
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
// other code here
}
這里 AppCompatDelegate.setDefaultNightMode() 方法可以接受的參數值有4個:
- MODE_NIGHT_NO. Always use the day (light) theme(一直應用日間(light)主題).
- MODE_NIGHT_YES. Always use the night (dark) theme(一直使用夜間(dark)主題).
- MODE_NIGHT_AUTO. Changes between day/night based on the time of day(根據當前時間在day/night主題間切換).
- MODE_NIGHT_FOLLOW_SYSTEM(默認選項). This setting follows the system’s setting, which is essentially MODE_NIGHT_NO(跟隨系統,通常為MODE_NIGHT_NO).
我們可以在任何時候調用這個方法,因為這個方法是靜態的。但是這個值并不是一直存在的,每次在開啟進程時需要重新設置。在上面的代碼中,我是在 onCreate() 方法中設置的,網上也有大神建議在Activity或者Application的 static 代碼塊中設置。如下所示:
public class App extends Application {
static {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
}
@Override
public void onCreate() {
super.onCreate();
// other code here
}
動態應用
雖然上面的靜態應用的設置非常簡單,但是這種方式的應用場景還是太少了。我們更多的還是需要動態的根據需要動態的切換。
-
檢測當前主題模式
int mode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
-
設置主題
if(mode == Configuration.UI_MODE_NIGHT_YES) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); } else if(mode == Configuration.UI_MODE_NIGHT_NO) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); } else { // blah blah } recreate();
在調用 recreate() 方法之前,還可以創建一些動畫進行過渡。而且,眾所周知,調用 recreate() 需要對一些數據進行保存,例如fragment,CheckBox,RadioBox等。如下所示:
public class MainFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { FragmentManager manager = getChildFragmentManager(); doubanMomentFragment = (DoubanMomentFragment) manager.getFragment(savedInstanceState, "douban"); } else { doubanMomentFragment = DoubanMomentFragment.newInstance(); } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); FragmentManager manager = getChildFragmentManager(); manager.putFragment(outState, "douban", doubanMomentFragment); }
我們也可以把主題的值存儲到SharedPreference中,已便于應用在下一次啟動時自動應用主題。
Q&A
-
Q:系統默認的顏色不合我的口味怎么辦?
A:使用主題屬性,例如: textColor:?android:attr/textColorPrimary , color:?attr/colorControlNormal 等。
-
Q:為什么我的WebView顏色沒有變化?
A:因為WebView不能使用主題屬性。WebView的顏色實際上取決于網頁內容顏色。網頁的默認背景色是白色,所以盡管設置了主題為 AppCompatDelegate.MODE_NIGHT_YES ,網頁仍然是白色,所以看起來就很不搭了。所以,網頁的內容和背景色等資源也需要適配了。
-
Q:為什么不直接設置為 MODE_NIGHT_AUTO 呢?
A:因為使用 MODE_NIGHT_AUTO 需要請求坐標權限,獲取系統的位置。你肯定會說了,這尼瑪不是坑爹嗎?如果程序已經授予了坐標權限(location permission)(如果你的target SDK為23或者更高,需要考慮運行時權限),AppCompat會試著去獲取上次保存的坐標,根據坐標來計算日出與日落的時間。如果程序沒有位置權限或者LocationManager沒有存儲上次坐標的信息呢?系統或默認設置為早上6點鐘為日出,下午10點為日落。用戶調整系統時間,當前的主題也會隨之改變。如果我們不希望用戶在設定主題后,主題還會隨著時間改變, MODE_NIGHT_AUTO 就不適用了。
運行效果
在Android 6.0及以下的設備上,本項目運行時會有切換的過渡動畫效果,但是不支持Android 7.0及以上的設備。