轉載請注明出處:http://blog.csdn.net/llew2011/article/details/51287396
在上篇文章Android 源碼系列之<五>從源碼的角度深入了解LayoutInflater.Factory之主題切換(中)我們實作了在目前Activity進行主題切換的功能,如果你還沒閱讀過上篇文章請點選這裡,在上篇文章結尾闡述了其中的不足,比如代碼通用性以及頁面跳轉之後進行主題切換,傳回之後無效果等,這篇文章主要是來解決以上問題的。
首先解決一下通用性的問題,在上文中如果Activity要實作主題切換都要寫一遍設定LayoutInflater的Factory邏輯,這個太麻煩了,假如我們APP中有一大堆Activity的話那不豈要寫一大遍重複代碼了?這不是我們的風格,是以先要提取這部分代碼放入基類BaseActivity中,然後子類直接繼承BaseActivity基類就好,代碼如下:
public abstract class BaseActivity extends Activity {
protected LayoutInflater mInflater;
protected SkinFactory mFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mFactory = new SkinFactory();
mInflater = getLayoutInflater();
mInflater.setFactory(mFactory);
}
}
BaseActivity中實作了設定LayoutInflater的Factory功能,在之後的開發中所有的Activity就直接繼承BaseActivity也就具備了主題切換的功能了。然後我們再來看一下之前BackgroundAtt的實作:
public class BackgroundAttr extends BaseAttr {
@Override
public void apply(View view) {
if(null != view) {
if(RES_ENTRY_TYPE_COLOR.equalsIgnoreCase(entryType)) {
view.setBackgroundColor(SkinManager.getInstance().getColor(attrValue));
} else if(RES_ENTRY_TYPE_DRAWABLE.equalsIgnoreCase(entryType)) {
view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(attrValue));
}
}
}
}
BackgroundAttr的實作就是來更改背景的,根據目前View的entryType來判斷類型,如果是更改背景顔色就調用setBackgroundColor()方法,否則如果是更改背景圖就調用setBackgroundDrawable()方法,那每一個BaseAttr的實作都需要做一次判斷代碼就是備援了,是以可以把判斷類型加入到基類BaseAttr中實作,代碼如下:
public abstract class BaseAttr {
public String attrName;
public int attrValue;
public String entryName;
public String entryType;
boolean isDrawableType() {
return "drawable".equalsIgnoreCase(entryType);
}
boolean isColorType() {
return "color".equalsIgnoreCase(entryType);
}
public abstract void apply(View view);
}
基類BaseAttr中定義好isDrawableType()和isColorType()方法之後就可以在子類中直接使用了,BackgroundAttr代碼如下所示:
public class BackgroundAttr extends BaseAttr {
@Override
public void apply(View view) {
if(null != view) {
if(isColorType()) {
view.setBackgroundColor(SkinManager.getInstance().getColor(attrValue));
} else if(isDrawableType()) {
view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(attrValue));
}
}
}
}
好了,重用代碼基本上已經完了,然後我們回頭看看有關切換主題遺留下的另外一個bug,先看一下這個bug是如何發生的,如圖所示:

根據運作效果看,當在第一個頁面設定完主題後跳轉到第二個頁面,在第二個頁面做了恢複預設主題操作,這時候傳回第一個頁面發現第一個頁面并沒有恢複成預設主題。這顯然是不正确的,發生這個問題的原因也比較好了解,就是說當我們做了主題切換後應該通知Activity,讓Activity做出響應。既然要通知Activity做出響應就應該知道有哪些Activity,是以需要緩存Activity。緩存Activity可以定義一個接口ISkinUpdate,讓Activity實作該接口,然後在SkinManager中緩存該接口的執行個體,當進行主題切換後依次通知緩存執行個體。接口定義如下:
public interface ISkinUpdate {
void updateSkin();
}
定義完接口之後,然後需要在SkinManager中新增一個緩存集合,對外提供新增和删除方法,代碼如下:
private List<ISkinUpdate> mObservers;
public void onAttach(ISkinUpdate observer) {
if(null == observer) return;
if(null == mObservers) {
mObservers = new ArrayList<ISkinUpdate>();
}
if(!mObservers.contains(observer)) {
mObservers.add(observer);
}
}
public void onDetach(ISkinUpdate observer) {
if(null == observer || null == mObservers) return;
mObservers.remove(observer);
}
SkinManager提供了onAttach()和onDetach()方法,添加完緩存功能之後,讓BaseActivity實作ISkinUpdate接口,然後在onResume()和onDestroy()方法中執行SkinManager的onAttach()和onDettach()方法,代碼如下:
public abstract class BaseActivity extends Activity implements ISkinUpdate {
protected LayoutInflater mInflater;
private SkinFactory mFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
mFactory = new SkinFactory();
mInflater = getLayoutInflater();
// 這裡通過反射修改mFactorySet的值,否則使用V7包的AppCompatActivity會抛異常
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(mInflater, false);
mInflater.setFactory(mFactory);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onResume() {
super.onResume();
SkinManager.getInstance().onAttach(this);
}
@Override
protected void onDestroy() {
destroySkinRes();
super.onDestroy();
}
public final void destroySkinRes() {
if(null != mFactory) {
mFactory.onDestroy();
}
mFactory = null;
mInflater = null;
SkinManager.getInstance().onDettach(this);
}
public final void createSkinView(View view, int id, AttrName attrName, EntryType entryType) {
mFactory.createSkinView(view, attrName, "", id, "", entryType);
}
@Override
public final void updateSkin() {
mFactory.applySkin();
};
}
有朋友回報說在使用V7下的AppCompatActivity時會抛異常,經過閱讀源碼發現是AppCompatActivity預設已經安裝了Factory了,如果LayoutInflater設定了Factory那麼再次為Factory指派會抛異常,而是否抛出異常是根據屬性mFactorySet來判定的,是以我們可以通過反射來修改mFactorySet的值進而防止抛異常(解決方式如上所示)。BaseActivity實作完該接口之後,在updateSkin()方法中調用mFactory的applySkin()方法輾轉通知View更改主題,運作一下看看效果:
有關頁面跳轉的問題算是解決了,但是還存在記憶體洩露的問題,因為每啟動一個Activity的時候都會建立一個Factory,然後我們在Factory中緩存了需要主題切換的View,是以需要在Activity的onDestroy()方法中清空Factory的緩存。在BaseActivity中添加方法如下:
public final void destroySkinRes() {
if(null != mFactory) {
mFactory.onDestroy();
}
mFactory = null;
mInflater = null;
SkinManager.getInstance().onDettach(this);
}
destroySkinRes()方法中調用了mFactroy的onDestroy()方法(該方法是清空緩存操作這裡不再貼出了)。後隻需在Activity的onDestroy()方法中調用該方法即可,代碼如下:
@Override
protected void onDestroy() {
destroySkinRes();
super.onDestroy();
}
接下來我們再考慮一個問題,如果我們在Activity的setContentView()中是直接通過new的方式建立View,然後把建立的View設定為目前Activity的顯示内容,這時候進行主題切換是不起作用的。示例如下:
public class ThirdActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView();
}
private void setContentView() {
FrameLayout titleLayout = new FrameLayout(this);
titleLayout.setBackgroundColor(getResources().getColor(R.color.common_title_bg_color));
TextView textView = new TextView(this);
textView.setText("第三個頁面");
textView.setTextColor(getResources().getColor(R.color.common_title_text_color));
textView.setTextSize(18);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2);
params.gravity = Gravity.CENTER;
titleLayout.addView(textView, params);
FrameLayout rootView = new FrameLayout(this);
rootView.setBackgroundColor(getResources().getColor(R.color.common_bg_color));
params = new FrameLayout.LayoutParams(-1, CommonUtil.dip2px(this, 65));
rootView.addView(titleLayout, params);
params = new FrameLayout.LayoutParams(-1, -1);
setContentView(rootView, params);
}
}
ThirdActivity雖然繼承了BaseActivity具有切換主題的功能,但是我們通過new的方式建立View然後當調用setContentView(View view)方法時并不會調用我們的Factory中的方法,既然不走Factory的onCreateView()方法,也就是說Factory沒法緩存到需要進行主題切換的View。知道了原因那問題就好解決了,我們可以手動的往Factory中添加需要主題切換的View,是以可以在基類BaseActivity中添加一個createSkinView()方法并設定其為final類型的(禁止子類重寫該方法),然後調用Factory的createSkinView()方法,代碼如下:
public final void createSkinView(View view, int id, AttrName attrName, EntryType entryType) {
mFactory.createSkinView(view, attrName, "", id, "", entryType);
}
createSkinView()方法接收4個參數,view表示需要緩存的view,id表示該view所引用的資源id,attrName和entryType定義為枚舉類型防止傳遞不支援的類型。然後調用mFactory的createSkinView()方法,createSkinView()方法如下:
public void createSkinView(View view, AttrName attrName, String attrValue, int id, String entryName, EntryType entryType) {
BaseAttr viewAttr = createAttr(attrName.toString(), attrValue, id, entryName, entryType.toString());
if(null != viewAttr) {
List<BaseAttr> viewAttrs = new ArrayList<BaseAttr>(1);
viewAttrs.add(viewAttr);
createSkinView(view, viewAttrs);
}
}
private void createSkinView(View view, List<BaseAttr> viewAttrs) {
SkinView skinView = new SkinView();
skinView.view = view;
skinView.viewAttrs = viewAttrs ;
mSkinViews.add(skinView);
if(SkinManager.getInstance().isExternalSkin()) {
skinView.apply();
}
}
添加完需要緩存View的方法之後,把ThirdActivity中需要主題切換的View加入到緩存集合中,代碼如下:
public class ThirdActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView();
}
private void setContentView() {
int id = R.color.common_title_bg_color;
FrameLayout titleLayout = new FrameLayout(this);
titleLayout.setBackgroundColor(getResources().getColor(id));
createSkinView(titleLayout, id, AttrName.background, EntryType.color);
id = R.color.common_title_text_color;
TextView textView = new TextView(this);
textView.setText("第三個頁面");
textView.setTextColor(getResources().getColor(id));
textView.setTextSize(18);
createSkinView(textView, id, AttrName.textColor, EntryType.color);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2);
params.gravity = Gravity.CENTER;
titleLayout.addView(textView, params);
id = R.color.common_bg_color;
FrameLayout rootView = new FrameLayout(this);
rootView.setBackgroundColor(getResources().getColor(id));
params = new FrameLayout.LayoutParams(-1, CommonUtil.dip2px(this, 65));
rootView.addView(titleLayout, params);
createSkinView(rootView, id, AttrName.background, EntryType.color);
params = new FrameLayout.LayoutParams(-1, -1);
setContentView(rootView, params);
}
}
在setContentView()中我們把需要進行主題切換的View調用createSkinView()方法加入到緩存集合中,其中需要注意傳遞參數的問題,下面看一下前後運作效果對比圖,如下所示:
通過運作效果圖對比就可以明确看出來設定生效了,需要注意的是目前隻是使用Activity做的實驗,如果項目中應用到了FragmentActivity、Fragment等需要做額外的處理。有關通過LayoutInflater的Factory方式實作主題切換功能就告一段落了,感謝收看。
另外私下趁熱打鐵解壓了QQ的安裝包,拿到了其主題切換需要用到的素材,模仿做了部分主題切換界面,效果如下:
源碼下載下傳