Android 源码系列之<六>从源码的角度深入理解LayoutInflater.Factory之主题切换(下)
2016-05-14 11:25
881 查看
转载请注明出处:/article/9966681.html
在上篇文章Android 源码系列之<五>从源码的角度深入理解LayoutInflater.Factory之主题切换(中)中我们实现了在当前Activity进行主题切换的功能,如果你还没阅读过上篇文章请点击这里,在上篇文章结尾阐述了其中的不足,比如代码通用性以及页面跳转之后进行主题切换,返回之后无效果等,这篇文章主要是来解决以上问题的。
首先解决一下通用性的问题,在上文中如果Activity要实现主题切换都要写一遍设置LayoutInflater的Factory逻辑,这个太麻烦了,假如我们APP中有一大堆Activity的话那不岂要写一大遍重复代码了?这不是我们的风格,因此先要提取这部分代码放入基类BaseActivity中,然后子类直接继承BaseActivity基类就好,代码如下:
基类BaseAttr中定义好isDrawableType()和isColorType()方法之后就可以在子类中直接使用了,BackgroundAttr代码如下所示:
根据运行效果看,当在第一个页面设置完主题后跳转到第二个页面,在第二个页面做了恢复默认主题操作,这时候返回第一个页面发现第一个页面并没有恢复成默认主题。这显然是不正确的,发生这个问题的原因也比较好理解,就是说当我们做了主题切换后应该通知Activity,让Activity做出响应。既然要通知Activity做出响应就应该知道有哪些Activity,所以需要缓存Activity。缓存Activity可以定义一个接口ISkinUpdate,让Activity实现该接口,然后在SkinManager中缓存该接口的实例,当进行主题切换后依次通知缓存实例。接口定义如下:
有朋友反馈说在使用V7下的AppCompatActivity时会抛异常,经过阅读源码发现是AppCompatActivity默认已经安装了Factory了,如果LayoutInflater设置了Factory那么再次为Factory赋值会抛异常,而是否抛出异常是根据属性mFactorySet来判定的,所以我们可以通过反射来修改mFactorySet的值从而防止抛异常(解决方式如上所示)。BaseActivity实现完该接口之后,在updateSkin()方法中调用mFactory的applySkin()方法辗转通知View更改主题,运行一下看看效果:
有关页面跳转的问题算是解决了,但是还存在内存泄露的问题,因为每启动一个Activity的时候都会创建一个Factory,然后我们在Factory中缓存了需要主题切换的View,所以需要在Activity的onDestroy()方法中清空Factory的缓存。在BaseActivity中添加方法如下:
createSkinView()方法接收4个参数,view表示需要缓存的view,id表示该view所引用的资源id,attrName和entryType定义为枚举类型防止传递不支持的类型。然后调用mFactory的createSkinView()方法,createSkinView()方法如下:
在setContentView()中我们把需要进行主题切换的View调用createSkinView()方法加入到缓存集合中,其中需要注意传递参数的问题,下面看一下前后运行效果对比图,如下所示:
通过运行效果图对比就可以明确看出来设置生效了,需要注意的是当前只是使用Activity做的实验,如果项目中应用到了FragmentActivity、Fragment等需要做额外的处理。有关通过LayoutInflater的Factory方式实现主题切换功能就告一段落了,感谢收看。
另外私下趁热打铁解压了QQ的安装包,拿到了其主题切换需要用到的素材,模仿做了部分主题切换界面,效果如下:
源码下载
在上篇文章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的安装包,拿到了其主题切换需要用到的素材,模仿做了部分主题切换界面,效果如下:
源码下载
相关文章推荐
- 安卓Drawable 小技巧以及建议收录1.
- Understand Android Activity's launchMode: standard, singleTop, singleTask and singleInstance
- 各分辨率图片生成神器
- getBackground().setAlpha所导致问题
- 如何自学Android
- android练习
- 我的Android进阶之旅------>Android studio 如何修改工程的包名
- Android studio中导入Vitamio支持库是遇到的问题
- TextView使用常见问题
- Android 编译ffmpeg
- Android开发初体验
- Android之canvas详解
- Android 编译x264
- AndroidStudio 安装各种错误解决-安装教程
- Android大神的博客
- Android小技巧
- Android中常见的布局文件的属性
- Android资源访问机制——获取Resources对象
- 【Android学习】案例中学习RxJava和Retrofit----妹纸+gank
- android和java平台统一的DES加密解决方案,解决加密不一样的问题