Android实现应用程序换肤解决方案(二)Demos
转载请注明出处: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资源管理框架的“灵魂”,如果巧妙的利用资源获取流程中的关键点,增加适配框架,能够让我们达到更换系统层的资源主题的目的。
程序源代码下载
更多相关文章
- Android(安卓)应用程序之间内容分享详解(二)
- Android(安卓)sharedUserId研究记录
- Android(安卓)之 Activity 生命周期
- Android应用程序及其主要结构
- Android应用屏幕适应问题的解决
- 运用smali自动注入技术分析android应用程序行为
- Android(安卓)SDK中 tools 目录下的工具介绍
- 3.5 意图Intent的概念
- Android应用程序的组成介绍