android换肤的实现方案,Android换肤技术总结

Android换肤技术总结

背景

纵观现在各种Android app,其换肤需求可以归为白天/黑夜主题切换(或者别的名字,通常2套),如同花顺/自选股/天天动听等,UI表现为一个switcher。

多种主题切换,通常为会员特权,如QQ/QQ空间。

对于第一种来说,目测应该是直接通过本地theme来做的,即所有图片/颜色的资源都在apk里面打包了。

而对于第二种,则相对复杂一些,由于作为一种线上服务,可能上架新皮肤,且那么多皮肤包放在apk里面实在太占体积了,所以皮肤资源会在选择后再进行下载,也就不能直接使用android的那套theme。

技术方案

内部资源加载方案和动态下载资源下载两种。

动态下载可以称为一种黑科技了,因为往往需要hack系统的一些方法,所以在部分机型和新的API上有时候可能有坑,但相对好处则很多图片/色值等资源由于是后台下发的,可以随时更新

APK体积减小

对应用开发者来说,换肤几乎是透明的,不需要关心有几套皮肤

可以作为增值服务卖钱!!

内部资源加载方案

内部资源加载都是通过android本身那套theme来做的,相对来说工作量更大,不同方案之间其实差别也不大,通常都是在BaseActivity里面做setTheme,主要差别是在实现即时刷新页面的策略上。

自定义view

MultipleTheme

做自定义view是为了在setTheme后会去立即刷新,更新页面UI对应资源(如TextView替换背景图和文字颜色),在上述项目中,则是通过对rootView进行遍历,对所有实现了ColorUiInterface的view/viewgroup进行setTheme操作来实现即使刷新的。

显然这样太重了,需要把应用内的各种view/viewgroup进行替换。

手动绑定view和要改变的资源类型

这个…我们看看用法吧….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的逻辑也很容易被弄得乱七八糟。

动态资源加载方案

resource替换

即覆盖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) <

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>> 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) <

}

// 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;

}

}

方法名字都改了

Hack Resources internally

黑科技方法,直接对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[] sPreloadedDrawables;

private static final LongSparseArray sPreloadedColorDrawables

= new LongSparseArray();

private static final LongSparseArray sPreloadedColorStateLists

= new LongSparseArray();

直接对Resources里面的这三个LongSparseArray进行替换,由于apk运行时的资源都是从这三个数组里面加载的,所以只要采用interceptor模式:public class DrawablePreloadInterceptor extends LongSparseArray

自己实现一个LongSparseArray,并通过反射set回去,就能实现换肤,具体getDrawable等方法里是怎么取preload数组的,可以自己看Resources的源码。

等等,就这么简单?,NONO,少年你太天真了,怎么去加载xml,9patch的padding怎么更新,怎么打包/加载自定义的皮肤包,drawable的状态怎么刷新,等等。这些都是你需要考虑的,在存在插件的app中,还需要考虑是否会互相覆盖resource id的问题,进而需要修改apt,把resource id按位放在2个range。

手Q和独立版QQ空间使用的是这种方案,效果挺好。

总结

尽管动态加载方案比较黑科技,可能因为系统API的更改而出问题,但相对来所自由度大,灵活性高,且相对透明。

内部加载方案大同小异,主要解决的都是即时刷新的问题,然而从目前的一些开源项目来看,仍然没有特别简便的方案。让我选的话,我宁愿让界面重新创建,比如重启activity,或者remove所有view再添加回来。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值