您的位置:首页 > 移动开发 > Android开发

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

2017-01-23 14:16 465 查看
作者: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的代码来看,设置壁纸确实好复杂,非常多个类,各种逻辑,要完全明白,真的有点困难。在这个设置壁纸上,也花费了较长时间进行分析。虽然现在也不是完全明白怎么设置的,但是通过各方面的测试,最终达到了效果。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐