前言
由于項目需要,近段時間開發的夜間模式功能。主流的方案如下:
1、通過切換theme實現
2、通過resource id映射實現
3、通過Android Support Library的實現
方案選擇
- 切換theme實現夜間模式
采用這種實現方式的代表是簡書和知乎~實現策略如下:
1)在xml中定義兩套theme,差別僅僅是顏色不同
<!--白天主題-->
<style name="DayTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="clockBackground">@android:color/white</item>
<item name="clockTextColor">@android:color/black</item>
</style>
<!--夜間主題-->
<style name="NightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/color3F3F3F</item>
<item name="colorPrimaryDark">@color/color3A3A3A</item>
<item name="colorAccent">@color/color868686</item>
<item name="clockBackground">@color/color3F3F3F</item>
<item name="clockTextColor">@color/color8A9599</item>
</style>
自定義顏色:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="clockBackground" format="color" />
<attr name="clockTextColor" format="color" />
</resources>
在layout布局文件中,如 TextView 里的 android:textColor="?attr/clockTextColor" 是讓其字體顏色跟隨所設置的 Theme。
2)Java代碼相關實現:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
initData();
initTheme();
setContentView(R.layout.activity_day_night);
initView();
}
在每次setContentView之前必須調用initTheme方法,因為當 View 創建成功后 ,再去 setTheme 是無法對 View 的 UI 效果產生影響的。
/**
* 刷新UI界面
*/
private void refreshUI() {
TypedValue background = new TypedValue();//背景色
TypedValue textColor = new TypedValue();//字體顏色
Resources.Theme theme = getTheme();
theme.resolveAttribute(R.attr.clockBackground, background, true);
theme.resolveAttribute(R.attr.clockTextColor, textColor, true);
mHeaderLayout.setBackgroundResource(background.resourceId);
for (RelativeLayout layout : mLayoutList) {
layout.setBackgroundResource(background.resourceId);
}
for (CheckBox checkBox : mCheckBoxList) {
checkBox.setBackgroundResource(background.resourceId);
}
for (TextView textView : mTextViewList) {
textView.setBackgroundResource(background.resourceId);
}
Resources resources = getResources();
for (TextView textView : mTextViewList) {
textView.setTextColor(resources.getColor(textColor.resourceId));
}
int childCount = mRecyclerView.getChildCount();
for (int childIndex = 0; childIndex < childCount; childIndex++) {
ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex);
childView.setBackgroundResource(background.resourceId);
View infoLayout = childView.findViewById(R.id.info_layout);
infoLayout.setBackgroundResource(background.resourceId);
TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname);
nickName.setBackgroundResource(background.resourceId);
nickName.setTextColor(resources.getColor(textColor.resourceId));
TextView motto = (TextView) childView.findViewById(R.id.tv_motto);
motto.setBackgroundResource(background.resourceId);
motto.setTextColor(resources.getColor(textColor.resourceId));
}
//讓 RecyclerView 緩存在 Pool 中的 Item 失效
//那么,如果是ListView,要怎么做呢?這里的思路是通過反射拿到 AbsListView 類中的 RecycleBin 對象,然后同樣再用反射去調用 clear 方法
Class<RecyclerView> recyclerViewClass = RecyclerView.class;
try {
Field declaredField = recyclerViewClass.getDeclaredField("mRecycler");
declaredField.setAccessible(true);
Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class<?>[]) new Class[0]);
declaredMethod.setAccessible(true);
declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]);
RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool();
recycledViewPool.clear();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
refreshStatusBar();
}
/**
* 刷新 StatusBar
*/
private void refreshStatusBar() {
if (Build.VERSION.SDK_INT >= 21) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = getTheme();
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId));
}
}
refreshUI函數起到模式切換的作用。通過 TypedValue 和 Theme.resolveAttribute 在代碼中獲取 Theme 中設置的顏色,來重新設置控件的背景色或者字體顏色等等。refreshStatusBar刷新狀態欄。
/**
* 展示一個切換動畫
*/
private void showAnimation() {
final View decorView = getWindow().getDecorView();
Bitmap cacheBitmap = getCacheBitmapFromView(decorView);
if (decorView instanceof ViewGroup && cacheBitmap != null) {
final View view = new View(this);
view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap));
ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
((ViewGroup) decorView).addView(view, layoutParam);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
objectAnimator.setDuration(300);
objectAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
((ViewGroup) decorView).removeView(view);
}
});
objectAnimator.start();
}
}
/**
* 獲取一個 View 的緩存視圖
*
* @param view
* @return
*/
private Bitmap getCacheBitmapFromView(View view) {
final boolean drawingCacheEnabled = true;
view.setDrawingCacheEnabled(drawingCacheEnabled);
view.buildDrawingCache(drawingCacheEnabled);
final Bitmap drawingCache = view.getDrawingCache();
Bitmap bitmap;
if (drawingCache != null) {
bitmap = Bitmap.createBitmap(drawingCache);
view.setDrawingCacheEnabled(false);
} else {
bitmap = null;
}
return bitmap;
}
showAnimation 是用于展示一個漸隱效果的屬性動畫,這個屬性作用在哪個對象上呢?是一個 View ,一個在代碼中動態填充到 DecorView 中的 View。
知乎之所以在夜間模式切換過程中會有漸隱效果,是因為在切換前進行了截屏,同時將截屏拿到的 Bitmap 設置到動態填充到 DecorView 中的 View 上,并對這個 View 執行一個漸隱的屬性動畫,所以使得我們能夠看到一個漂亮的漸隱過渡的動畫效果。而且在動畫結束的時候再把這個動態添加的 View 給 remove 了,避免了 Bitmap 造成內存飆升問題。
- resource id映射實現夜間模式
通過id獲取資源時,先將其轉換為夜間模式對應id,再通過Resources來獲取對應的資源。
public static Drawable getDrawable(Context context, int id) {
return context.getResources().getDrawable(getResId(id));
}
public static int getResId(int defaultResId) { if (!isNightMode()) {
return defaultResId;
}
if (sResourceMap == null) {
buildResourceMap();
}
int themedResId = sResourceMap.get(defaultResId);
return themedResId == 0 ? defaultResId : themedResId;
}
private static void buildResourceMap() {
sResourceMap = new SparseIntArray();
sResourceMap.put(R.drawable.common_background, R.drawable.common_background_night);
// ...
}
這個方案簡單粗暴,麻煩的地方和第一種方案一樣:每次添加資源都需要建立映射關系,刷新UI的方式也與第一種方案類似,貌似今日頭條,網易新聞客戶端等主流新聞閱讀應用都是通過這種方式實現的夜間模式。
- 通過Android Support Library實現
1)在res目錄中為夜間模式配置專門的目錄,以-night為后綴
2)在Application中設置夜間模式
3)夜間模式切換
夜間模式實現
三種方案比較,第二種太暴力,不適合項目后期開發;第一種方法需要做配置的地方比第三種方法多。總體來說,第三種方法最簡單,類似整個app內有一個夜間模式的總開關,切換了以后就不用管了。最后采用第三種方案!
通過Android Support Library實現夜間模式雖然簡單,但是當中也碰到了一些坑。現做一下記錄:
1、 橫屏切換的時候,夜間模式混亂
基于Android Support Library的夜間模式,相當于是support庫來幫忙關鍵相關的資源,有時候會出現錯誤的情況。比如說app橫豎屏切換之后!!經測試發現,每次調起一個橫屏的Activity,然后退出,整個app的夜間模式就亂了,部分的UI調用的是日間模式的資源~~~
這里認為的加了一個多余的設定:
/**
* 刷新UI_MODE模式
*/
public void refreshResources(Activity activity) {
if (Prop.isNightMode.getBoolean()) {
updateConfig(activity, Configuration.UI_MODE_NIGHT_YES);
} else {
updateConfig(activity, Configuration.UI_MODE_NIGHT_NO);
}
}
/** * google官方bug,暫時解決方案 * 手機切屏后重新設置UI_MODE
模式(因為在DayNight主題下,切換橫屏后UI_MODE會出錯,會導致
資源獲取出錯,需要重新設置回來)
*/
private void updateConfig(Activity activity, int uiNightMode) {
Configuration newConfig = new
Configuration(activity.getResources().getConfiguration());
newConfig.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
newConfig.uiMode |= uiNightMode;
activity.getResources().updateConfiguration(newConfig, null);
}
在每次退出橫屏的時候,調用這個方法,強制刷新一次config
2、 drawable xml文件中部分顏色值 日間/夜間 弄反了
Android Support Library實現的夜間模式,資源的獲取碰到了一些坑。我們經常會在drawable文件夾中定義一些xml來做背景形狀、背景顏色。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="false" android:state_enabled="true">
<shape android:shape="rectangle">
<solid android:color="@color/app_textbook_bg_color"/>
<corners android:radius="5dp" />
</shape>
</item>
</selector>
按照Android Support Library的介紹,需要為夜間模式創建一個為night借我的目錄,那么這里就可以有兩種理解:
1)在values/color.xml 和values-night/color.xml分別為app_textbook_bg_color定義不同的色值
2)在values/color.xml 和values-night/color.xml分別為app_textbook_bg_color定義不同的色值;此外,需要分別定義drawable/bg_textbook.xml和drawable-night/bg_textbook.xml,兩個文件的內容可以一樣。
這里碰到了一些坑。原先采用的是第一種方法,這樣代碼改動少,看起來一目了然。但是,,不同廠商的手機會有不一樣的表現!!部分手機,在夜間模式的時候還是用的日間的資源;殺了app重進才會好。
我的理解是Android會對資源做緩存~ 緩存的時候會將app_textbook_color解析出來并緩存;假設日間模式app_textbook_color為#FFFFFF,我們設置夜間模式切換,這時候不同手機廠商的策略不一樣,有些廠商會把緩存清除,所以切成夜間模式的時候app_textbook_color的色值會改變,夜間模式正常;但是有些廠商應該不會清理緩存,夜間模式切換之后,拿的是日間模式緩存下的色值,也就是#FFFFFF,這樣就出問題了~~
以上為個人見解,建議碰到這種情況,多在drawable下寫一個xml防止個別手機出錯。
3、切換夜間模式需要restartActivity,會閃一下
這也是一個比較坑的地方。夜間模式切換以后,需要重新獲取一遍資源,最簡單的方法是restart一下。現在我采用的就是這種簡單粗暴的方法,用戶體驗比較不友好,后期需要參考知乎的實現,改進實現。
參考鏈接
Android夜間模式最佳實踐
知乎和簡書的夜間模式實現套路
AppCompat v23.2 - 夜間模式,你所不知道的坑