博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android换肤技术总结
阅读量:6426 次
发布时间:2019-06-23

本文共 6256 字,大约阅读时间需要 20 分钟。

原文出处:

背景

纵观现在各种Android app,其换肤需求可以归为

  • 白天/黑夜主题切换(或者别的名字,通常2套),如同花顺/自选股/天天动听等,UI表现为一个switcher。

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

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

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

技术方案

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

动态下载可以称为一种黑科技了,因为往往需要hack系统的一些方法,所以在部分机型和新的API上有时候可能有坑,但相对好处则很多

  • 图片/色值等资源由于是后台下发的,可以随时更新

  • APK体积减小

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

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

内部资源加载方案

内部资源加载都是通过android本身那套theme来做的,相对业务开发来说工作量更大(需要定义attr和theme),不同方案类似地都是在BaseActivity里面做setTheme,差别主要在解决以下2个问题的策略:

  • setTheme后如何实时刷新,而不用重新创建页面(尤其是listview里面的item)。

  • 哪些view需要刷新,刷新什么(背景?字体颜色?ImageView的src?)。

自定义view

做自定义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) << 32) | value.data;    Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);    if (dr != null) {        return dr;    }    ...    ...    return dr;}

而5.1代码里Resources.java:

// 可以看到,方法参数里面加上了Themepublic 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) << 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; } }

方法名字都改了

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的更改而出问题,但相对来所

好处有

  • 灵活性高,后台可以随时更新皮肤包

  • 相对透明,开发者几乎不用关心有几套皮肤,不用去定义各种theme和attr,甚至连皮肤包的打包都- - 可以交给设计或者专门的同学

  • apk体积节省

    存在的问题

  • 没有完善的开源项目,如果我们采用动态加载的第二种方案,需要的项目功能包括:

  • 自定义皮肤包结构

  • 换肤引擎,加载皮肤包资源并load,实时刷新。

  • 皮肤包打包工具

  • 对各种rom的兼容

如果有这么一个项目的话,就一劳永逸了,有兴趣的同学可以联系一下,大家一起搞一搞。

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

转载地址:http://difga.baihongyu.com/

你可能感兴趣的文章
本期最新 9 篇论文,帮你完美解决「读什么」的问题 | PaperDaily #19
查看>>
图解SSIS监视文件夹并自动导入数据
查看>>
Lucene.Net 2.3.1开发介绍 —— 四、搜索(一)
查看>>
MyBatis Review——开发Dao的方法
查看>>
技术研发国产化进程加快 看传感器企业如何展示十八般武艺
查看>>
技术助力第三次革命
查看>>
《HTML与CSS入门经典(第8版)》——2.6 总结
查看>>
新手指南:在 Ubuntu 和 Fedora 上安装软件包
查看>>
在 CentOS7.0 上搭建 Chroot 的 Bind DNS 服务器
查看>>
大型网站的 HTTPS 实践(二):HTTPS 对性能的影响
查看>>
《Swift 权威指南》——第6章,第6.10节嵌套函数
查看>>
《自己动手做交互系统》——1.3 本章小结
查看>>
Mobile devices bundled with malware?
查看>>
《JavaScript面向对象精要》——1.5 访问属性
查看>>
《Python数据可视化编程实战》—— 第 1 章 准备工作环境
查看>>
Android应用性能优化最佳实践.1.1 Android Studio的优势
查看>>
《设计模式解析(第2版•修订版)》—第2章 2.2节什么是UML
查看>>
【直播】APP全量混淆和瘦身技术揭秘
查看>>
10个大坑,当你产品上架AppStore会遇到
查看>>
【shell 脚本】两种登录方式
查看>>