转载请注明出处:http://blog.csdn.net/droyon/article/details/9454679

之前上传过一篇帖子(应用程序更换皮肤解决方案一:http://blog.csdn.net/hailushijie/article/details/9427651),描述了利用Style样式解决在应用程序内部实现换主题或者换皮肤的功能。虽然能够实现我们想要的功能,但皮肤资源打包在主应用程序的内部,操作上不够灵活,并且只能更换apk应用提供的几种有限的换肤主题。

我们来说说第一种方案的优缺点:

优点:简单,便于维护

缺点:主题固定写死在程序内部,增加皮肤需要改动代码(设计的目的在于在增加新功能,实现新需求时,尽可能少的改动代码。原因很简单,改动代码就存在引入bug的风险)。

我们今天介绍的这种解决方案,能够动态的检测当前手机中的资源apk包,并提供接口让用户进行皮肤的更换。这种方式解决了第一种方案中不灵活的地方。

这种方案的优缺点:

优点:增加皮肤灵活,便于扩展,而且不需要改动源代码。

缺点:资源文件独立于应用程序apk。(独立于apk安装在手机中,这不是优点吗?为什么说是缺点,关于原因,稍后说明

应用程序运行截图:

主界面:默认主题


主界面:加载主题,并且预览主题,应用主题界面


主界面:应用主题之后的界面(主题2)


主界面:应用主题之后的界面(主题3)


主要代码:

1、首先我们先明确一下apk应用程序资源是如何读取的:

Resources resolver = context.getResources();if(DEBUG)Log.d(LOG_TAG, "ResourceConfig loadParticularStyle");mActivityBackground = resolver.getDrawable(R.drawable.bj);
首先我们应该通过Context得到一个Resource对象。至于Resource对象为什么能够getDrawable,getString方法,我们在此不做说明。这些更深层的东西属于android资源管理机制。

我们可以得出一个结论:想要读取外部apk应用程序的资源(图片,文字),我们首先应该得到外部apk应用的Resource对象,根本上是得到外部apk应用程序的Context对象。

2、如何得到外部apk应用程序的Context对象那?

虽然本人生平说过无数的谎话,但是这一个我认为是最完美的(大话西游)。虽然Context虚类提供了很多个方法,但是下面这一个我认为用在此处是最完美的。

public abstract class Context {....../**     * Return a new Context object for the given application name.  This     * Context is the same as what the named application gets when it is     * launched, containing the same resources and class loader.  Each call to     * this method returns a new instance of a Context object; Context objects     * are not shared, however they share common state (Resources, ClassLoader,     * etc) so the Context instance itself is fairly lightweight.     *     * <p>Throws {@link PackageManager.NameNotFoundException} if there is no     * application with the given package name.     *     * <p>Throws {@link java.lang.SecurityException} if the Context requested     * can not be loaded into the caller's process for security reasons (see     * {@link #CONTEXT_INCLUDE_CODE} for more information}.     *     * @param packageName Name of the application's package.     * @param flags Option flags, one of {@link #CONTEXT_INCLUDE_CODE}     *              or {@link #CONTEXT_IGNORE_SECURITY}.     *     * @return A Context for the application.     *     * @throws java.lang.SecurityException     * @throws PackageManager.NameNotFoundException if there is no application with     * the given package name     */    public abstract Context createPackageContext(String packageName,            int flags) throws PackageManager.NameNotFoundException;......}
Context对象在它的很多的子类中提供了这个方法,这个方法允许我们通过应用程序的packageName来创建外部应用的Context对象。

现在这个问题解决了,我们可以在我们的Activity或者任意能够获取本应用程序Context的地方,调用这个方法,传入外部应用程序的packageName,就可以得到我们需要的目标--对方应用程序的Context对象。

3、我们应该提供搜索功能,加载手机系统中安装的所有的、本应用程序能够识别的皮肤应用,进而得到他们的packageName,进而通过2中提供的说明构建外部apk应用的Context,进而得到外部对象的Resource对象,进而能够加载外部apk的资源。

关于加载系统内部所有安装的皮肤apk应用程序:

private ArrayList<ThemeNameAndPackageName> getPackageInstallName(){Log.d(LOG_TAG, "getPackageInstallName...");ArrayList<ThemeNameAndPackageName> arrayList = new ArrayList<ThemeNameAndPackageName>();List<PackageInfo> mPackageInfo = mPackageM.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);//得到系统中所有的安装应用信息,这个方法,flag参数传递不好,导致Binder传递太多数据,引出bug,这个flag也不好for(PackageInfo info : mPackageInfo){//遍历所有应用,找到符合规则的packageName,加入到List中。if(info.packageName!=null&&info.packageName.startsWith(mPackageName)){ThemeNameAndPackageName item = new ThemeNameAndPackageName();String name = null;if(info.packageName.equals(mFreFragment.getActionPackageName())){mSelectedThemeAndPackageName = item;}if(info.packageName.equals(mPackageName)){name = SettingsTheme.this.getString(R.string.theme_local);}else{try {Context packageContext =SettingsTheme.this.createPackageContext(info.packageName, Context.CONTEXT_IGNORE_SECURITY);name = packageContext.getString(info.applicationInfo.labelRes);} catch (NameNotFoundException e) {name = SettingsTheme.this.getString(R.string.theme_outer);e.printStackTrace();}}item.setName(name);item.setPackageName(info.packageName);Log.d(LOG_TAG, "packageInfo toString is:"+item);arrayList.add(item);}}return arrayList;}
什么样的主题应用才是我们能够识别的那?

我们的主应用程序的包名:

package com.example.themeandroid;
我们在此定义一个规则:所有的资源主题apk的包名,以主应用程序的包名为前缀。例如:

package="com.example.themeandroid.skin.num1"
我们遍历系统中安装的应用,我们就可以将所有以com.example.themeandroid为前缀包名的应用程序加载出来,因为他们就是我们的主题包(包括:自身主题包和外部主题包)。

4、如果外部主题包卸载了,或者外部主题包没有我们需要加载的资源(通过外部应用Context加载资源,会抛出异常),我们应该“退而求其次”加载自身的主题。

private Context createRightContext(){Context rightContext = mContext;String packageName = Utils.getPersistPackageContextName(mContext);if(DEBUG)Log.d(LOG_TAG, "ResourceConfig packageName is:"+packageName+",,,Context packageName is:"+mContext.getPackageName());if(packageName != null){try {rightContext = rightContext.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY);//见2中介绍,创建外部apk应用程序的Contextreturn rightContext;} catch (NameNotFoundException e) {e.printStackTrace();if(DEBUG)Log.d(LOG_TAG, "packageName '"+packageName+"' create context fail!!!");}}return rightContext;}
5、注意事项。之前我们说这个方案有个缺点,那就是资源不在主应用程序自身内,为什么说它是一个缺点那?
我们知道,在一个应用程序打包成apk过程中,会首先对应用程序的apk资源(图片,文字,xml,attr)打包(关于应用程序编译流程: http://blog.csdn.net/hailushijie/article/category/1358744),然后生成一个R文件,这个R文件中有应用程序内部定义的各种资源值,在通过Context.getResource().getString(R.string.xxx)时,默认传入的R.String.xxx的值就来自于这个R文件。

public final class R {    public static final class attr {    }    public static final class drawable {        public static final int bj=0x7f020000;        public static final int btn=0x7f020001;        public static final int btn_switch_normal=0x7f020002;        public static final int btn_switch_pressed=0x7f020003;        public static final int button_switcher_drawable=0x7f020004;        public static final int green_divider=0x7f020005;        public static final int ic_launcher=0x7f020006;    }    public static final class id {        public static final int btn04=0x7f050003;        public static final int btn_comfirm=0x7f050001;        public static final int fre_view=0x7f050000;        public static final int root=0x7f050002;    }    public static final class layout {        public static final int af=0x7f030000;        public static final int main=0x7f030001;    }    public static final class string {        public static final int app_label=0x7f040000;        public static final int app_name=0x7f040001;        public static final int button_show_style=0x7f040002;        public static final int button_theme_confirm=0x7f040003;        public static final int list_empty=0x7f040008;        public static final int theme_fre_view=0x7f040004;        public static final int theme_local=0x7f040006;        public static final int theme_outer=0x7f040007;        public static final int theme_settings=0x7f040005;    }}

<string name="app_label">换肤解决方案(二)</string>

上面是应用程序的打包后生成的R文件,以及string.xml资源文件部分代码。

到现在为止,你可能有点懵,这和我们要实现的功能有关系吗?

等我问大家一个问题,大家估计就知道有没有关系了,我的问题是:

问题:(本人叙述能力不强,问题描述有点罗嗦,见谅!!!)我们通过Context.getResource().getString(R.String.app_label);得出app_label对应的字符串的值,也就是“换肤解决方案(二)”   见上文 附。换句话说:Context.getResource().getString(0x7f040000) ==  “换肤解决方案(二)”我们此时的Context是我们主应用程序本身,可如果我们读取的是外部的apk应用程序,那么Context就是外部apk应用程序的Context,问题来了,假如外部应用程序存在<string name="app_label">换肤解决方案(二)</string>定义,可编译产生的R文件中的值却为    public static final int app_label=0x7f050001;,那么我们通过Context.getResource().getString(0x7f40000);就得不出我们想要的值,这该怎么办?
在android资源管理机制中,我们知道framework层的资源为系统资源,在编译时对每一类资源分配了特定的id。

例如:0x01030000:其中前两位01代表是framework层的系统资源,03:代表资源类型(Style),后面的四位代表资源编号。

资源类型:

attr = 01id = 02Style = 03String = 04dimmen = 05color = 06array = 07drawable = 08layout = 09anim = 0a
可在我们编译应用程序时,不同android版本可能存在差异,但在4.0版本上,应用程序的编译出的R文件随着资源的不同而不同(此处的不同,指的是资源类型,例如,本来04代表string,可在R文件中04可能用来代表layout资源)。

因此我们通过Context.getResource().getString(R.string.xxx)获取资源不一定准确。

解决方案:

1、主应用程序存在的资源类型,你一定要存在。(注意是资源类型,至于内容,就可以自己定义了)
2、通过反射机制加载资源,而不是通过Context.getResource().getString()方式。


好了,介绍到此结束吧,稍后会把源代码打包,提供下载测试。demo存在bug,另外关于实现方式等问题,欢迎大家交流指正。

应用程序换肤,我介绍了两种方案,这两种方案都可以达到我们的目的。然而这两种方案都没有触及android资源管理框架的“灵魂”,如果巧妙的利用资源获取流程中的关键点,增加适配框架,能够让我们达到更换系统层的资源主题的目的。

程序源代码下载

更多相关文章

  1. Android(安卓)应用程序之间内容分享详解(二)
  2. Android(安卓)sharedUserId研究记录
  3. Android(安卓)之 Activity 生命周期
  4. Android应用程序及其主要结构
  5. Android应用屏幕适应问题的解决
  6. 运用smali自动注入技术分析android应用程序行为
  7. Android(安卓)SDK中 tools 目录下的工具介绍
  8. 3.5 意图Intent的概念
  9. Android应用程序的组成介绍

随机推荐

  1. android:windowSoftInputMode用法
  2. [Android] 环境配置之Android(安卓)Studi
  3. android layout布局属性
  4. Android学习笔记之mainfest文件中android
  5. android布局属性详解
  6. spring android 编译环境搭建
  7. Android笔记: Android版本号
  8. android 实现qq聊天对话界面效果
  9. Android虚拟键盘挡住输入组件的解决办法
  10. Android之打开闪光灯关键代码