您的位置:首页 > 移动开发 > Android开发

Android 源码系列之<六>从源码的角度深入理解LayoutInflater.Factory之主题切换(下)

2016-05-14 11:25 881 查看
转载请注明出处:/article/9966681.html

在上篇文章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的安装包,拿到了其主题切换需要用到的素材,模仿做了部分主题切换界面,效果如下:



源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: