Android系统更换主题外观的实现方法

作者:Liuhua Chen

 

一、   实现思路

安卓应用在读取资源时是由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的代码来看,设置壁纸确实好复杂,非常多个类,各种逻辑,要完全明白,真的有点困难。在这个设置壁纸上,也花费了较长时间进行分析。虽然现在也不是完全明白怎么设置的,但是通过各方面的测试,最终达到了效果。

你可能感兴趣的:(Android系统更换主题外观的实现方法)