原文出处:
http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/java
纵观如今各类Android app,其换肤需求能够归为android
白天/黑夜主题切换(或者别的名字,一般2套),如同花顺/自选股/每天动听等,UI表现为一个switcher。git
多种主题切换,一般为会员特权,如QQ/QQ空间。github
对于第一种来讲,目测应该是直接经过本地theme来作的,即全部图片/颜色的资源都在apk里面打包了。
而对于第二种,则相对复杂一些,因为做为一种线上服务,可能上架新皮肤,且那么多皮肤包放在apk里面实在太占体积了,因此皮肤资源会在选择后再进行下载,也就不能直接使用android的那套theme。数组
内部资源加载方案和动态下载资源下载两种。
动态下载能够称为一种黑科技了,由于每每须要hack系统的一些方法,因此在部分机型和新的API上有时候可能有坑,但相对好处则不少app
图片/色值等资源因为是后台下发的,能够随时更新工具
APK体积减少性能
对应用开发者来讲,换肤几乎是透明的,不须要关心有几套皮肤字体
能够做为增值服务卖钱!!优化
内部资源加载都是经过android自己那套theme来作的,相对业务开发来讲工做量更大(须要定义attr和theme),不一样方案相似地都是在BaseActivity里面作setTheme,差异主要在解决如下2个问题的策略:
setTheme后如何实时刷新,而不用从新建立页面(尤为是listview里面的item)。
哪些view须要刷新,刷新什么(背景?字体颜色?ImageView的src?)。
MultipleTheme
作自定义view是为了在setTheme后会去当即刷新,更新页面UI对应资源(如TextView替换背景图和文字颜色),在上述项目中,则是经过对rootView进行遍历,对全部实现了ColorUiInterface的view/viewgroup进行setTheme操做来实现即便刷新的。
显然这样过重了,须要把应用内的各类view/viewgroup进行替换。
手动绑定view和要改变的资源类型
Colorful
这个…咱们看看用法吧…
ViewGroupSetter listViewSetter = new ViewGroupSetter(mNewsListView); // 绑定ListView的Item View中的news_title视图,在换肤时修改它的text_color属性 listViewSetter.childViewTextColor(R.id.news_title, R.attr.text_color); // 构建Colorful对象来绑定View与属性的对象关系 mColorful = new Colorful.Builder(this) .backgroundDrawable(R.id.root_view, R.attr.root_view_bg) // 设置view的背景图片 .backgroundColor(R.id.change_btn, R.attr.btn_bg) // 设置背景色 .textColor(R.id.textview, R.attr.text_color) .setter(listViewSetter) // 手动设置setter .create(); // 设置文本颜色
我就是想换个皮肤,还得在activity里本身去设置要改变哪一个view的什么属性,对应哪一个attribute?是否是成本过高了?并且activity的逻辑也很容易被弄得乱七八糟。
开源项目可参照Android-Skin-Loader
即覆盖application的getResource方法,优先加载本地皮肤包文件夹下的资源包,对于性能问题,能够经过attribute或者资源名称规范(如须要换肤则用skin_开头)来优化,从而不对不换肤的资源进行额外开销。
能够重点关注该项目中的SkinInflaterFactory和SkinManager(实现了本身的getColor、getDrawable方法)。
不过因为Android 5.1源码里,getDrawable方法的实现被修改了,因此会致使没法跟肤的问题(实际上是loadDrawable被修改了,连参数都改了,相似的内部API大改在5.1上还不少)。
4.4的源码中Resources.java:
public Drawable getDrawable(int id) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; if (value == null) { value = new TypedValue(); } else { mTmpValue = null; } getValue(id, value, true); } // 实际资源经过loadDrawable方法加载 Drawable res = loadDrawable(value, id); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; } } return res; } // loadDrawable会去preload的LongSparseArray里面查找 /*package*/ Drawable loadDrawable(TypedValue value, int id) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) android.util.Log.d("PreloadDrawable", name); } } boolean isColorDrawable = false; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; } final long key = isColorDrawable ? value.data : (((long) value.assetCookie) << 32) | value.data; Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key); if (dr != null) { return dr; } ... ... return dr; }
而5.1代码里Resources.java:
// 能够看到,方法参数里面加上了Theme public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; if (value == null) { value = new TypedValue(); } else { mTmpValue = null; } getValue(id, value, true); } final Drawable res = loadDrawable(value, id, theme); synchronized (mAccessLock) { if (mTmpValue == null) { mTmpValue = value; } } return res; } /*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException { if (TRACE_FOR_PRELOAD) { // Log only framework resources if ((id >>> 24) == 0x1) { final String name = getResourceName(id); if (name != null) { Log.d("PreloadDrawable", name); } } } final boolean isColorDrawable; final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches; final long key; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { isColorDrawable = true; caches = mColorDrawableCache; key = value.data; } else { isColorDrawable = false; caches = mDrawableCache; key = (((long) value.assetCookie) << 32) | value.data; } // First, check whether we have a cached version of this drawable // that was inflated against the specified theme. if (!mPreloading) { final Drawable cachedDrawable = getCachedDrawable(caches, key, theme); if (cachedDrawable != null) { return cachedDrawable; } }
方法名字都改了
黑科技方法,直接对Resources进行hack,Resources.java:
// Information about preloaded resources. Note that they are not // protected by a lock, because while preloading in zygote we are all // single-threaded, and after that these are immutable. private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables; private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>(); private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>();
直接对Resources里面的这三个LongSparseArray进行替换,因为apk运行时的资源都是从这三个数组里面加载的,因此只要采用interceptor模式:
public class DrawablePreloadInterceptor extends LongSparseArray<Drawable.ConstantState>
本身实现一个LongSparseArray,并经过反射set回去,就能实现换肤,具体getDrawable等方法里是怎么取preload数组的,能够本身看Resources的源码。
等等,就这么简单?,NONO,少年你太天真了,怎么去加载xml,9patch的padding怎么更新,怎么打包/加载自定义的皮肤包,drawable的状态怎么刷新,等等。这些都是你须要考虑的,在存在插件的app中,还须要考虑是否会互相覆盖resource id的问题,进而须要修改apt,把resource id按位放在2个range。
手Q和独立版QQ空间使用的是这种方案,效果挺好。
尽管动态加载方案比较黑科技,可能由于系统API的更改而出问题,但相对来所
好处有
灵活性高,后台能够随时更新皮肤包
相对透明,开发者几乎不用关心有几套皮肤,不用去定义各类theme和attr,甚至连皮肤包的打包都- - 能够交给设计或者专门的同窗
apk体积节省
存在的问题
没有完善的开源项目,若是咱们采用动态加载的第二种方案,须要的项目功能包括:
自定义皮肤包结构
换肤引擎,加载皮肤包资源并load,实时刷新。
皮肤包打包工具
对各类rom的兼容
若是有这么一个项目的话,就一劳永逸了,有兴趣的同窗能够联系一下,你们一块儿搞一搞。
内部加载方案大同小异,主要解决的都是即时刷新的问题,然而从目前的一些开源项目来看,仍然没有特别简便的方案。让我选的话,我宁愿让界面从新建立,好比重启activity,或者remove全部view再添加回来。