纵观现在各种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

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

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

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

Colorful

这个…我们看看用法吧….

1
2
3
4
5
6
7
8
9
10
11
12
13
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方法,实现自己的resource,优先加载本地皮肤包文件夹下的资源包,对于性能问题,可以通过attribute或者资源名称规范(如需要换肤则用skin_开头)来优化,从而不对不换肤的资源进行额外检查开销。

不过由于Android5.1源码里,drawable初始化的时候调用的是loadDrawable,而不是resource.getDrawable,而loadDrawable是私有的方法,无法覆盖,所以虽然很方便,却无法继续使用(不用关心任何皮肤相关的事情,android:color指定颜色就行了,神奇滴会自动换肤)。

自定义LayoutInflator.Factory

开源项目可参照Android-Skin-Loader

即setFactory使用自定义的LayoutInflator.Factory,可以重点关注该项目中的SkinInflaterFactorySkinManager(实现了自己的getColor、getDrawable、getBitmap、getColorStateList等等方法)。

需要自定义一个tag比如app:customStyle,重写所有的style,转成set方法,这样带来的牺牲就是增加了换肤的成本,要写很多style,自己去set,并不完全透明了。

Hack Resources internally

黑科技方法,直接对Resources进行hack,Resources.java:

1
2
3
4
5
6
7
8
// 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模式:

1
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再添加回来(或者你可能想遍历rootview,然后一个个检查是否需要换肤然后set…)。

Android项目中的换肤总结的更多相关文章

  1. Android 切换主题以及换肤的实现

    Android 切换主题以及换肤的实现 一.介绍 现在市面上有很多 APP 有切换主题和皮肤的功能!特别是阅读类的 APP! 上面两张图分别是 知乎 APP 和Fuubo APP的两张截图!都带有切换 ...

  2. android项目中配置NDK自动编译生成so文件

    1 下载ndk开发包   2 在android 项目中配置编译器(以HelloJni项目为例)  2.1 创建builer  (a)Project->Properties->Builder ...

  3. Android 项目中文件夹的说明与作用(转)

    (转自:http://blog.csdn.net/goodshot/article/details/11529731) Android 项目中文件夹的作用 1. src:存放所有的*.java源程序. ...

  4. android XMl 解析神奇xstream 一: 解析android项目中 asset 文件夹 下的 aa.xml 文件

    简介 XStream 是一个开源项目,一套简单实用的类库,用于序列化对象与 XML 对象之间的相互转换. 将 XML 文件内容解析为一个对象或将一个对象序列化为 XML 文件. 1.下载工具 xstr ...

  5. 对Android项目中的文件夹进行解释

    对Android项目中的文件夹进行解释: · src:里面存放的是Activity程序,或者是以后的其他组件,在此文件夹之中建立类的时候一定要注意,包名称不能是一级. · gen:此文件夹中的内容是自 ...

  6. android项目中如何加载已有so库 <转>

    1,在项目根目录下建立文件夹libs/armeabi文件夹 2,将so库放入 libs/armeabi文件夹 注意事项: 1,如果采用静态注册的方式请注意C文件中严格按照命名规则 Java_packa ...

  7. 在Android项目中启用Java 8的部分特性--Lambda & Method References

    Android N发布时同时发布了一个新的编译工具Jack(AS2.1+支持),基于Jack我们可以使用Java 8 的部分特性,在低版本机器上能使用的更少,同时Jack也有诸多不完善,工具链的改变难 ...

  8. Android项目中如何用好构建神器Gradle?(转)

    最近在忙团队并行开发的事情,主要是将各个团队的代码分库,一方面可以降低耦合,为后面模块插件化做铺垫,另一方面采用二进制编译,可以加快编译速度.分库遇到了一些问题,很多都要通过Gradle脚本解决,所以 ...

  9. android项目中刷新activity界面

    android项目中在sqlite数据库插入/更新/删除数据后: 1. 刷新当前activity界面数据(手动刷新): 在activity类下新增一个refresh()方法: /** * 刷新, 这样 ...

随机推荐

  1. Gitlab备份与恢复[七]

    标签(linux): git 笔者Q:972581034 交流群:605799367.有任何疑问可与笔者或加群交流 备份 配置文件中加入 gitlab_rails['bakup_path']='/da ...

  2. Eclipse使用EGit,commit之后仍显示NO HEAD的解决方法

    由于以前做的项目一直用的都是svn,想试一下git尝尝鲜,遇见点问题.记录下来防止以后再出现这种情况,同时希望能帮助到和我一样的初学者. 问题描述: 右键项目-->team-->commi ...

  3. rabbitMQ的安装和配置(一)

    erlang是一门面向并发的编程语言,流行的消息队列rabbitMQ是基于erlang环境运行的: 系统环境 操作系统:oracle-linux7.3 erlang版本:otp_src_20.0 下载 ...

  4. mongodb- Java API 查询操作

    package com.x.mongodb; import java.net.UnknownHostException;import java.util.Collection;import java. ...

  5. 关于css选择器的一些细节

    1.如何区分一个html标签的不同样式 使用标签名.类名的方式解决 如果希望特别强调其中的某一个或几个元素,处理的方案有三个: 1.id选择器 2.class选择器 3.层级选择器 看下面的代码: & ...

  6. loadrunner调用jar包方法

    环境 win7(32位)/winXP+loadrunner11+JDK 1.6(一定要配置JAVA环境变量) 1.Eclipse中创建com.medivh包 package com.medivh; p ...

  7. 用icas下载文件报错

    前段时间服务器升级,过程中测试到报表下载,报表下载要用到icas,用的是sdts-client.jar,但是此jar包有两种连接模式,分别为: public static final SdtsConn ...

  8. Java中的递归调用

    Java中不合理的使用递归调用,可能会导致栈内存溢出,这点是需要注意的. java将为每个线程维护一个栈,栈里将为每个方法保存一个栈帧,栈帧代表了一个方法的运行状态. 也就是我们常说的方法栈.最后一个 ...

  9. POJ 2653 Pick-up sticks [线段相交 迷之暴力]

    Pick-up sticks Time Limit: 3000MS   Memory Limit: 65536K Total Submissions: 12861   Accepted: 4847 D ...

  10. Java实现单链表的快速排序和归并排序

    本文描述了LeetCode 148题 sort-list 的解法. 题目描述如下: Sort a linked list in O(n log n) time using constant space ...