Android 系统主题更换功能的原理

解宏扬
2023-12-01

一、   实现思路

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

 类似资料: