主题更换的实现实录

  1. 主题更换的的实现方案。

采用截断请求,资源重定位的方式来达到更换系统全套资源的目的。

  1. 实现思路及过程:

通过分析andrid里面setting的语言切换机制,决定采用Configuration的skin属性的改变对应用户切换操作,并将由用户操作引起Configuration的skin的变化值传到ActivityManagerService里,并把变化值传给Resources对象,Resources对象根据configuration的skin值做资源的重定位加载。这样做的好处是:系统已经做好了Configuration的变化响应事件,不用开发者去理会,应用的重启并重新加载资源。


2.1用户接口的设计

对于用户来讲,需要设计一个用户接口,这里写写个简单的主题切换管理工具,用户可以通过对apk的操作,改变系统样式,具体界面简单设计如下:



 图中的default,Theme1分别代表了两个不同的主题,放在一个listView中,数据的来源是通过解析我指定目录下的Theme包。当用户点击Theme1,让系统重新去加载我自定义的主题包中的资源。

2.2资源包格式设定

对应要换资源的apk,在自己定义的Theme目录下提供相同包名的目录文件,drawable资源保持跟原始apk相同的路径及资源名,另外在自定义的目录提供统一其他资源的文件,文件名字统一命名为“theme_values.xml”,这个文件提供除drawable资源以外的其他资源。

例如:

<Boyue_Theme_Values>
<colorname="divider_color">#00ff00</color>
<colorname="testColor">#ff0000</color>
<stringname="app_name">BoyueTheme1</string>
<stringname="hello_world">Hello world!1</string>
<stringname="action_settings">Settings1</string>
<stringname="default_name">Default1</string>
<stringname="custom_theme_name">New [%1$s]1</string>
<stringname="loading">Loading...1</string>
<stringname="test"> hello world1</string>
<dimenname="activity_horizontal_margin">160dp</dimen>
<dimenname="activity_vertical_margin">160dp</dimen>
<dimenname="textsize">40sp</dimen>
</Boyue_Theme_Values>

 


  1. 3 对framework进行修改,截断资源请求,实现资源请求的重定位。

这里主要修改Resources,AssetManager,TypedArray.

注意:Resources中获取资源的方法,有些只针对手写的代码生效,对于xml中view的属性的资源的解析在TypedArray中。



  1. 编码实现。

App:

1.指定theme的存放目录,并解析指定目录下的Theme数,并存放到contentProvider中。(contentProvider的设计是因为原来主题包采用的是apk的方式,为了不同进程之间的共享设定,现在没有必要)

2.查询指定URI的contentProvider的Theme数据添加到用户接口ListView中。

3.给listview添加Item的点击事件,并将用户点击的Item数据进行一定的数据整理,将用户的点击的Item对应的Theme的全路径作为skin,并把变化的skin传到ActivityManagerService中进行处理。


Framework:

  1. 判断当前应用的Configuration的skin的值是否为系统的默认值,如果不是加载skin(用户选择Theme)目录下的对应应用的资源(包括:color,string,dimens),保存到3个不同HashMap中。另外要注意一点:当传过来的skin与当前的skin不同时,需要清空原来的缓存的map,一遍让3个map加载最新传进来的skin的资源。
  2. 截断请求,对于get资源方法的修改,当get通过id获取到原有资源时,判断skin是否已经改变了,如已改变,让他加载skin目录下自己包下的资源达到资源切换。

根据以上步骤只能完成的效果基本上是:

资源只有在代码中明确指示用Resources的get资源的方法得到时,才能更换成我们想要的资源。可能产生的问题有:

  1. xml里面view子节点的属性的值,不同通过修改Resources类的资源请求方法达到重定位。
    2.launcher里面的应用的icon不能实时更新(这里基本可以确定应用是有重新启动去加载新的资源,但惟有launcher没有效果)


下面继续:

问题一的解决思路:

通过测试,只有在.java代码中明确view的资源加载,resources类写的重定位资源的方法才能生效。而界面布局xml里面配置的属性值却不能。这就需要去了解界面布局xml的解析以及xml下各个节点view的创建和初始化过程,xml的解析详细了解可以去看罗升阳-android应用程序资源的查找过程,看完之后你可能只是能了解到xml的解析及各个节点view的创建,在LayoutInflater中有“return(View) constructor.newInstance(args);”这里他结束了分析。但是如果要实现资源的替换,我们还必须继续深究,最后这句话的意思其实就是view对象通过args参数描述的属性值进行初始化。了解这个我们就可以明白自定义的View必须传递两个参数context和AttributeSet这两个参数了.接着我们就可以去找带上面两个参数的view的构造函数了,这里我们需要了解android

View的继承关系如下图所示:


Android各个view之间的关系清晰明了。

回到找view构造函数的过程:我们可以找到下面这段代码:

finalint N = a.getIndexCount();
for(int i = 0; i < N; i++) {
intattr = a.getIndex(i);
switch(attr) {
casecom.android.internal.R.styleable.View_background:
background= a.getDrawable(attr);
break;
casecom.android.internal.R.styleable.View_padding:


由于代码过长,我就截取了一小部分,读者可以自己找到这段代码玩下看。这里主要是显示view类处理了哪些属性值,这里是没有我们需要改变的属性的,读者看完就会知道,我们对于高级属性像textColor,TextSize是基于TextView这层,找到TextView,可以很快定位到两个参数的构造方法,下面就以textSizt为例:

找到TextView的构造函数,可以很快定位到下面这段代码:

casecom.android.internal.R.styleable.TextView_textSize:
textSize= a.getDimensionPixelSize(attr, textSize);
break;

A是一个TypedArray对象,通过调用getDimensionPixelSize()来获取xmL里面属性的设置的值。通过这段代码我们就明白,view创建的属性的加载是TypedArray类来管理的,找到TypedArray对应的方法进行自己的逻辑代码处理即可了。


问题二的解决思路:

  1. 在传递Configuration.skin这个变量值到ActivityManagerService下updateConfiguration方法中这里处理configuration的改变情况.按照这个思路修改完framework后,各个app都会重新启动,但是launcherapp由于对Icon和label有缓存,所以需要判断当Configuration.skin改变后要清除其缓存。

在修改framework上有个地方没有理解,附上代码:

  1. 在acitiviytManagerservice类中updateConfigurationLocked方法最后发送的广播的意图。以上没弄懂的地方不影响整个系统的执行过程.所以

总的情况就是当应用的Configuration对象的值有发生变化,即更新Configuration,并重新启动app,加载资源.