天天看點

Android 源碼系列之<六>從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)

        轉載請注明出處: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是如何發生的,如圖所示:

Android 源碼系列之&lt;六&gt;從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)

        根據運作效果看,當在第一個頁面設定完主題後跳轉到第二個頁面,在第二個頁面做了恢複預設主題操作,這時候傳回第一個頁面發現第一個頁面并沒有恢複成預設主題。這顯然是不正确的,發生這個問題的原因也比較好了解,就是說當我們做了主題切換後應該通知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更改主題,運作一下看看效果:

Android 源碼系列之&lt;六&gt;從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)

        有關頁面跳轉的問題算是解決了,但是還存在記憶體洩露的問題,因為每啟動一個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()方法加入到緩存集合中,其中需要注意傳遞參數的問題,下面看一下前後運作效果對比圖,如下所示:

Android 源碼系列之&lt;六&gt;從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)
Android 源碼系列之&lt;六&gt;從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)

        通過運作效果圖對比就可以明确看出來設定生效了,需要注意的是目前隻是使用Activity做的實驗,如果項目中應用到了FragmentActivity、Fragment等需要做額外的處理。有關通過LayoutInflater的Factory方式實作主題切換功能就告一段落了,感謝收看。

        另外私下趁熱打鐵解壓了QQ的安裝包,拿到了其主題切換需要用到的素材,模仿做了部分主題切換界面,效果如下:

Android 源碼系列之&lt;六&gt;從源碼的角度深入了解LayoutInflater.Factory之主題切換(下)

源碼下載下傳