一、 实现思路
安卓应用在读取资源时是由AssetManager和Resources两个类来实现的。Resouce类是先根据ID来找到资源文件名称,然后再将该文件名称交给AssetManager来打开文件。我们主题开发的核心思路就是在应用读取资源时,先去主题包里读取资源,若有资源,直接返回主题包的资源,若无资源,直接返回应用本身的资源。
参考博客:http://blog.csdn.net/luoshengyang/article/details/8806798
二、 方案实现
a) 修改源码Resource.java
private LoadResourcesCallBack mCallBack;
public interface LoadResourcesCallBack{
ColorStateListloadColorStateList(TypedValue value, int id);
Drawable loadDrawable(TypedValue value, int id);
XmlResourceParser loadXmlResourceParser(int id, String type);
int getDimensionPixelSize(int index);
int getDimensionPixelOffset(int index);
float getDimension(int index);
Integer getInteger(int index);
}
public LoadResourcesCallBack getLoadResourcesCallBack() {
return mCallBack;
}
public boolean regitsterLoadResourcesCallBack(LoadResourcesCallBackcallBack) {
if (callBack== null || mCallBack != null) {
return false;
}
mCallBack = callBack;
return true;
}
在Resouces类是主要加这个接口,这个接口就是主题改变的关键所在。接口LoadResourcesCallBack中的抽象方法,从名子中可以发现,Resources类中也有同名的方法。LoadResourcesCallBack中的方法就是在Resources同名方法中调用,即Resouces类中在执行这些方法时,会先执行LoadResourcesCallBack实现的方法。
LoadResourcesCallBack抽象方法实现如下:
context.getResources().regitsterLoadResourcesCallBack(new Resources.LoadResourcesCallBack() {
@Override
public ColorStateListloadColorStateList(TypedValue typedValue, int i) {
if (i == 0) return null;
String entryName = context.getResources().getResourceEntryName(i);
ColorStateList color = ResourceManagerTPV.getInstance(context).getColorStateList(entryName);
return color;
}
@Override
public DrawableloadDrawable(TypedValue typedValue, int i) {
if(i == 0){
return null;
}
String entryName = context.getResources().getResourceEntryName(i);
Drawable drawable = ResourceManagerTPV.getInstance(context).getDrawable(entryName);
return drawable;
}
@Override
public XmlResourceParserloadXmlResourceParser(int i, String s) {
return null;
}
@Override
public int getDimensionPixelSize(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
int dimensionPixelSize = ResourceManagerTPV.getInstance(context).getDimensionPixelSize(entryName);
return dimensionPixelSize;
}
@Override
public int getDimensionPixelOffset(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getDimensionPixelOffset(entryName);
}
@Override
public float getDimension(int i) {
if(i == 0){
return -1;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getDimen(entryName);
}
@Override
public IntegergetInteger(int i) {
if(i == 0){
return null;
}
String entryName = context.getResources().getResourceEntryName(i);
return ResourceManagerTPV.getInstance(context).getInteger(entryName);
}
});
实现原理:在查找资源时会根据提供的ID进行查找,然后通过资源ID查找资源ID对应的资源名称,然后获取当前设置的主题包的Context,然后再由主题包的Context通过资源名称查找当前主题包下是否有要查询的资源,有就返回具体资源的值,如图片,就返回Drawable资源,没有就返回Null,Resouce类就会执行自己的方法。
b) 通知更新
实现原理:参考系统语言切换的实现方法。
参考博客:http://blog.csdn.net/wxlinwzl/article/details/42614117
三、 开发中遇到的问题
a) 开发框架的设计
在方案实行前,已讨论过主题的实现方案,刚开始实现方案与台北TPV的主题实现方案类似,都是先制作一个默认的主题包。但是在制作完主题包后,发现该方案,资源为只读,不能同时支持多个主题动态切换,且在XML中不能直接引用。后来就参考了TUF的做法,觉得这个方案可行,就按这个方案来实行。
b) 代码中读取color资源,仍是应用本身的资源,不是主题包的资源
在与Launcher和SystemUI调试时,同样的方法,在代码读取图片和Color值时,图片会读取到安装的主题包下的资源,而Color资源没有读取到,仍是应用本身设置的值。后来发现是在Resources源码中getColor,如下红色字体代码中,还没有判断是用哪个资源时,已经直接返回应用的Color资源。最后解决方法,在该段代码前进行判断。
@ColorInt
public int getColor(@ColorRes int id,@Nullable Theme theme) throws NotFoundException {
……..
if (value.type >=TypedValue.TYPE_FIRST_INT
&& value.type <=TypedValue.TYPE_LAST_INT) {
mTmpValue = value;
return value.data;
} else if (value.type !=TypedValue.TYPE_STRING) {
throw new NotFoundException(
"Resource ID#0x" + Integer.toHexString(id) + " type #0x"
+ Integer.toHexString(value.type) + " isnot valid");
}
mTmpValue = null;
}
…….
final ColorStateList csl =loadColorStateList(value, id, theme);
……..
}
c) SystemUI不会更新
SystemUI是一个很特殊的应用,切换资源时,无法同步切换,只能通过重启手机才会更新资源,而重启手机又与UX的设计不一致。最后只能通过广播通知其更新。
d) Widget应用无法更新
在与Clock调试时,同样的方法,同样的步骤,在widget中就是不切换资源,最后也只能像SystemUI一样通过广播进行更新。只有一个Clock,那时感觉用这种方法,也还好,修改的代码不多,也不繁琐。后来,天气也需要更新,问题就来了。天气widget的实现方法与Clock不一样,而且天气设置图片的方法也不一样。若是天气也是接收广播,然后再自己代码中更新,修改的代码量非常多,且本身天气应用的逻辑就比较复杂,如此下去不是一个可行的实现方案,不排除以后其他的Widget也需要修改,所以这个方案不能实行,只能另辟方法。
所以就一直去看看Widget的实现原理,发现Widget都是通过RemoteView来远程代理的。就去查看RemoteView的源码,
private View inflateView(Contextcontext, RemoteViews rv, ViewGroupparent) {
// RemoteViews may be built by an application installedin another
// user. So build a context thatloads resources from that user but
// still returns the current usersuserId so settings like data / time formats
// are loaded without requiring crossuser persmissions.
final ContextcontextForResources = getContextForResources(context);
Context inflationContext = new ContextWrapper(context){
@Override
public ResourcesgetResources() {
return contextForResources.getResources();
}
@Override
public Resources.ThemegetTheme() {
return contextForResources.getTheme();
}
@Override
public StringgetPackageName() {
return contextForResources.getPackageName();
}
};
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correctcontext and
// we don't add a filter to thestatic version returned by getSystemService.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
return inflater.inflate(rv.getLayoutId(), parent, false);
}
通过这个方法开头的说明,这个方法就是RemoteView获取资源的关键所在,我们只要在getResources()方法中注册一下Resources类中自定义的接口。事实证明,确实是如此,修改了此次代码后,Clock和天气都不需要执行任何操作。主题市场只需要统计需要修改的Color和Drawable的名称。
e) 锁屏壁纸和桌面壁纸与效果不一致
壁纸的设置,系统有提供一些接口进行设置。该开始就用了系统提供的接口进行设置,发现桌面的壁纸不会动,而且显示得很模糊,锁屏的壁纸模糊效果与壁纸不在同一个位置,出现分层。显然这样的设置方法是不可行的,询问了Launcher负责人,具体Launcher的裁剪方法也不是非常清楚。最后自己去下载了Launcher的代码来看,设置壁纸确实好复杂,非常多个类,各种逻辑,要完全明白,真的有点困难。在这个设置壁纸上,也花费了较长时间进行分析。虽然现在也不是完全明白怎么设置的,但是通过各方面的测试,最终达到了效果。