轉(zhuǎn)載自http://blog.coderclock.com/2016/08/28/android/知乎和簡(jiǎn)書(shū)的夜間模式實(shí)現(xiàn)套路/
說(shuō)到夜間模式疹尾,在網(wǎng)上看到很多童鞋都說(shuō)用什么什么框架來(lái)實(shí)現(xiàn)這個(gè)功能芬首,然后仔細(xì)去看一下各個(gè)推薦的框架澳窑,發(fā)現(xiàn)其實(shí)都是動(dòng)態(tài)換膚的,動(dòng)態(tài)換膚可比夜間模式要復(fù)雜多了,未免大材小用了。說(shuō)實(shí)話培遵,我一直沒(méi)用什么好思路,雖然網(wǎng)上有童鞋提供了一種思路是通過(guò) setTheme
然后再 recreate Activity
的方式登刺,但是這樣帶來(lái)的問(wèn)題是非常多的籽腕,看起來(lái)就相當(dāng)不科學(xué)(為什么不科學(xué),后文會(huì)說(shuō))纸俭。于是皇耗,直接想到了去逆向分析那些夜間模式做得好的應(yīng)用的源代碼,學(xué)習(xí)他們的實(shí)現(xiàn)套路揍很。所以郎楼,本文的實(shí)現(xiàn)思路來(lái)自于編寫(xiě)這些應(yīng)用的夜間模式功能的童鞋,先在這里向他們表示感謝女轿。我的手機(jī)里面使用高頻的應(yīng)用不少箭启,其中簡(jiǎn)書(shū)和知乎是屬于夜間模式做得相當(dāng) nice 的。先給兩個(gè)效果圖大家對(duì)比感受下
如果大家仔細(xì)觀察蛉迹,肯定會(huì)發(fā)現(xiàn),知乎的切換效果更漂亮些放妈,因?yàn)樗幸粋€(gè)漸變的效果北救。那么它們的夜間模式到底是如何實(shí)現(xiàn)的呢?別急接著往下看芜抒,你也可以珍策。
實(shí)現(xiàn)套路
這里先展示一下我的實(shí)現(xiàn)效果吧
此處分為兩個(gè)部分,一部分是
xml
文件中要干的活宅倒,一部分是 Java
代碼要實(shí)現(xiàn)的活攘宙,先說(shuō) xml
吧。
XML 配置
首先拐迁,先寫(xiě)一套UI界面出來(lái)蹭劈,上方左邊是兩個(gè) TextView
,右邊是兩個(gè) CheckBox
线召,下方是一個(gè) RecyclerView
铺韧,實(shí)現(xiàn)很簡(jiǎn)單,這里我不貼代碼了缓淹。
接著哈打,在
styles
文件中添加兩個(gè) Theme
塔逃,一個(gè)是日間主題,一個(gè)是夜間主題料仗。它們的屬性都是一樣的湾盗,唯一區(qū)別在于顏色效果不同。
<!--白天主題-->
<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>
需要注意的是立轧,上面的 clockTextColor
和 clockBackground
是我自定義的 color
類(lèi)型屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="clockBackground" format="color" />
<attr name="clockTextColor" format="color" />
</resources>
然后再到所有需要實(shí)現(xiàn)夜間模式功能的 xml
布局文件中淹仑,加入類(lèi)似下面設(shè)置,比如我在 RecyclerView
的 Item
布局文件中做了如下設(shè)置
稍稍解釋下其作用肺孵,如
TextView
里的 android:textColor=”?attr/clockTextColor”
是讓其字體顏色跟隨所設(shè)置的 Theme
匀借。到這里,xml
需要做的配置全部完成平窘,接下來(lái)是 Java
代碼實(shí)現(xiàn)了吓肋。
Java 代碼實(shí)現(xiàn)
大家可以先看下面的實(shí)現(xiàn)代碼,看不懂的童鞋可以邊結(jié)合我代碼下方實(shí)現(xiàn)思路解說(shuō)瑰艘。
package com.clock.study.activity;
import ...
/**
* 夜間模式實(shí)現(xiàn)方案
*
* @author Clock
* @since 2016-08-11
*/
public class DayNightActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {
private final static String TAG = DayNightActivity.class.getSimpleName();
/**用于將主題設(shè)置保存到SharePreferences的工具類(lèi)**/
private DayNightHelper mDayNightHelper;
private RecyclerView mRecyclerView;
private LinearLayout mHeaderLayout;
private List<RelativeLayout> mLayoutList;
private List<TextView> mTextViewList;
private List<CheckBox> mCheckBoxList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
initData();
initTheme();
setContentView(R.layout.activity_day_night);
initView();
}
private void initView() {
mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
mRecyclerView.setAdapter(new SimpleAuthorAdapter());
mHeaderLayout = (LinearLayout) findViewById(R.id.header_layout);
mLayoutList = new ArrayList<>();
mLayoutList.add((RelativeLayout) findViewById(R.id.jianshu_layout));
mLayoutList.add((RelativeLayout) findViewById(R.id.zhihu_layout));
mTextViewList = new ArrayList<>();
mTextViewList.add((TextView) findViewById(R.id.tv_jianshu));
mTextViewList.add((TextView) findViewById(R.id.tv_zhihu));
mCheckBoxList = new ArrayList<>();
CheckBox ckbJianshu = (CheckBox) findViewById(R.id.ckb_jianshu);
ckbJianshu.setOnCheckedChangeListener(this);
mCheckBoxList.add(ckbJianshu);
CheckBox ckbZhihu = (CheckBox) findViewById(R.id.ckb_zhihu);
ckbZhihu.setOnCheckedChangeListener(this);
mCheckBoxList.add(ckbZhihu);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int viewId = buttonView.getId();
if (viewId == R.id.ckb_jianshu) {
changeThemeByJianShu();
} else if (viewId == R.id.ckb_zhihu) {
changeThemeByZhiHu();
}
}
private void initData() {
mDayNightHelper = new DayNightHelper(this);
}
private void initTheme() {
if (mDayNightHelper.isDay()) {
setTheme(R.style.DayTheme);
} else {
setTheme(R.style.NightTheme);
}
}
/**
* 切換主題設(shè)置
*/
private void toggleThemeSetting() {
if (mDayNightHelper.isDay()) {
mDayNightHelper.setMode(DayNight.NIGHT);
setTheme(R.style.NightTheme);
} else {
mDayNightHelper.setMode(DayNight.DAY);
setTheme(R.style.DayTheme);
}
}
/**
* 使用簡(jiǎn)書(shū)的實(shí)現(xiàn)套路來(lái)切換夜間主題
*/
private void changeThemeByJianShu() {
toggleThemeSetting();
refreshUI();
}
/**
* 使用知乎的實(shí)現(xiàn)套路來(lái)切換夜間主題
*/
private void changeThemeByZhiHu() {
showAnimation();
toggleThemeSetting();
refreshUI();
}
/**
* 刷新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,要怎么做呢紫新?這里的思路是通過(guò)反射拿到 AbsListView 類(lèi)中的 RecycleBin 對(duì)象均蜜,然后同樣再用反射去調(diào)用 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(typedValuse.resourceId));
}
}
/**
* 展示一個(gè)切換動(dòng)畫(huà)
*/
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();
}
}
/**
* 獲取一個(gè) 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;
}
}
實(shí)現(xiàn)思路和代碼解說(shuō):
1.DayNightHelper
類(lèi)是用于保存夜間模式設(shè)置到 SharePreferences
的工具類(lèi),在 initData
函數(shù)中被初始化芒率,其他的 View
和 Layout
都是界面布局囤耳,在 initView
函數(shù)中被初始化;
2.在 Activity
的 onCreate
函數(shù)調(diào)用 setContentView
之前偶芍,需要先去 setTheme
充择,因?yàn)楫?dāng) View
創(chuàng)建成功后 ,再去 setTheme
是無(wú)法對(duì) View
的 UI
效果產(chǎn)生影響的匪蟀;
3.onCheckedChanged
用于監(jiān)聽(tīng)日間模式和夜間模式的切換操作椎麦;
4.refreshUI
是本實(shí)現(xiàn)的關(guān)鍵函數(shù),起著切換效果的作用材彪,通過(guò) TypedValue
和 Theme.resolveAttribute
在代碼中獲取 Theme
中設(shè)置的顏色观挎,來(lái)重新設(shè)置控件的背景色或者字體顏色等等。需要特別注意的是 RecyclerView
和 ListView
這種比較特殊的控件處理方式段化,代碼注釋中已經(jīng)說(shuō)明嘁捷,大家可以看代碼中注釋;
5.refreshStatusBar
用于刷新頂部通知欄位置的顏色穗泵;
6.showAnimation
和 getCacheBitmapFromView
同樣是本實(shí)現(xiàn)的關(guān)鍵函數(shù)普气,getCacheBitmapFromView
用于將 View
中的內(nèi)容轉(zhuǎn)換成 Bitmap
(類(lèi)似于截屏操作那樣),showAnimation
是用于展示一個(gè)漸隱效果的屬性動(dòng)畫(huà)佃延,這個(gè)屬性作用在哪個(gè)對(duì)象上呢现诀?是一個(gè) View
夷磕,一個(gè)在代碼中動(dòng)態(tài)填充到 DecorView
中的 View
(不知道 DecorView
的童鞋得回去看看 Android Window
相關(guān)的知識(shí))。知乎之所以在夜間模式切換過(guò)程中會(huì)有漸隱效果仔沿,是因?yàn)樵谇袚Q前進(jìn)行了截屏坐桩,同時(shí)將截屏拿到的 Bitmap
設(shè)置到動(dòng)態(tài)填充到 DecorView
中的 View
上,并對(duì)這個(gè) View
執(zhí)行一個(gè)漸隱的屬性動(dòng)畫(huà)封锉,所以使得我們能夠看到一個(gè)漂亮的漸隱過(guò)渡的動(dòng)畫(huà)效果绵跷。而且在動(dòng)畫(huà)結(jié)束的時(shí)候再把這個(gè)動(dòng)態(tài)添加的 View
給 remove
了,避免了 Bitmap
造成內(nèi)存飆升問(wèn)題成福。對(duì)待知乎客戶(hù)端開(kāi)發(fā)者這種處理方式碾局,我必須雙手點(diǎn)贊外加一個(gè)大寫(xiě)的服。
到這里奴艾,實(shí)現(xiàn)套路基本說(shuō)完了净当,簡(jiǎn)書(shū)和知乎的實(shí)現(xiàn)套路如上所述,區(qū)別就是知乎多了個(gè)截屏和漸隱過(guò)渡動(dòng)畫(huà)效果而已蕴潦。
一些思考
整理逆向分析的過(guò)程像啼,也對(duì)夜間模式的實(shí)現(xiàn)有了不少思考,希望與各位童鞋們探討分享潭苞。
最初步的逆向分析過(guò)程就發(fā)現(xiàn)了忽冻,知乎和簡(jiǎn)書(shū)并沒(méi)有引入任何第三方框架來(lái)實(shí)現(xiàn)夜間模式,為什么呢此疹?
因?yàn)槲铱吹降拇蟛糠侄紝?shí)現(xiàn)夜間模式的思路都是用開(kāi)源的換膚框架僧诚,或多或少存在著些 BUG。簡(jiǎn)書(shū)和知乎不用可能是出于框架不穩(wěn)定性秀菱,以及我前面提到的用換膚框架來(lái)實(shí)現(xiàn)夜間模式大材小用吧振诬。(我也只是瞎猜,哈哈哈)
前面我提到衍菱,通過(guò)
setTheme
然后再去Activity recreate
的方案不可行,為什么呢肩豁?
我認(rèn)為不可行的原因有兩點(diǎn)脊串,一個(gè)是 Activity recreate
會(huì)有閃爍效果體驗(yàn)不加,二是 Activity recreate
涉及到狀態(tài)狀態(tài)保存問(wèn)題清钥,如自身的狀態(tài)保存琼锋,如果 Activity
中包含著多個(gè) Fragment
,那就更加頭疼了祟昭。
知乎和簡(jiǎn)書(shū)設(shè)置夜間模式的位置缕坎,有點(diǎn)巧妙,巧妙在哪篡悟?
知乎和簡(jiǎn)書(shū)出發(fā)夜間模式切換的地方谜叹,都是在 MainActivity
的一個(gè) Fragment
中匾寝。也就是說(shuō),如果你要切換模式時(shí)荷腊,必須回到主界面艳悔,此時(shí)只存在主界面一個(gè) Activity
,只需要遍歷主界面更新控件色調(diào)即可女仰。而對(duì)于其他設(shè)置夜間模式后新建的 Activity
猜年,只需要在 setContentView
之前做一下判斷并 setTheme
即可。
總結(jié)
關(guān)于簡(jiǎn)書(shū)和知乎夜間模式功能實(shí)現(xiàn)的套路就講解到這里疾忍,整個(gè)實(shí)現(xiàn)套路都是我通過(guò)逆向分析簡(jiǎn)書(shū)和知乎的代碼取得乔外,這里再一次向?qū)崿F(xiàn)這些代碼的童鞋以示感謝。當(dāng)然一罩,上面的代碼我是經(jīng)過(guò)精簡(jiǎn)提煉過(guò)的杨幼,在原先簡(jiǎn)書(shū)和知乎客戶(hù)端中的實(shí)現(xiàn)代碼還做了相應(yīng)的抽象設(shè)計(jì)和遞歸遍歷等等,這里是為了方便講解而做了精簡(jiǎn)擒抛。如果有童鞋喜歡這種實(shí)現(xiàn)套路推汽,也可以自己加以抽象封裝。這里也推薦各位童鞋一個(gè)我常用的思路歧沪,就是當(dāng)你對(duì)一個(gè)功能沒(méi)有思路時(shí)歹撒,大可找一些實(shí)現(xiàn)了這類(lèi)功能的優(yōu)秀應(yīng)用進(jìn)行逆向代碼分析。需要實(shí)現(xiàn)代碼的童鞋诊胞,可以訪問(wèn):https://github.com/D-clock/AndroidStudyCode