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是如何发生的,如图所示:

        根据运行效果看,当在第一个页面设置完主题后跳转到第二个页面,在第二个页面做了恢复默认主题操作,这时候返回第一个页面发现第一个页面并没有恢复成默认主题。这显然是不正确的,发生这个问题的原因也比较好理解,就是说当我们做了主题切换后应该通知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的安装包,拿到了其主题切换需要用到的素材,模仿做了部分主题切换界面,效果如下:




源码下载



发布了39 篇原创文章 · 获赞 87 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/llew2011/article/details/51287396