转载请注明出处:http://blog.youkuaiyun.com/droyon/article/details/9454679
之前上传过一篇帖子(应用程序更换皮肤解决方案一:http://blog.youkuaiyun.com/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);
我们可以得出一个结论:想要读取外部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;
- ......
- }
现在这个问题解决了,我们可以在我们的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;
- package="com.example.themeandroid.skin.num1"
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应用程序的Context
- return rightContext;
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- if(DEBUG)Log.d(LOG_TAG, "packageName '"+packageName+"' create context fail!!!");
- }
- }
- return rightContext;
- }
我们知道,在一个应用程序打包成apk过程中,会首先对应用程序的apk资源(图片,文字,xml,attr)打包(关于应用程序编译流程: http://blog.youkuaiyun.com/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);就得不出我们想要的值,这该怎么办?
例如:0x01030000:其中前两位01代表是framework层的系统资源,03:代表资源类型(Style),后面的四位代表资源编号。
资源类型:
- attr = 01
- id = 02
- Style = 03
- String = 04
- dimmen = 05
- color = 06
- array = 07
- drawable = 08
- layout = 09
- anim = 0a
因此我们通过Context.getResource().getString(R.string.xxx)获取资源不一定准确。
解决方案:
1、主应用程序存在的资源类型,你一定要存在。(注意是资源类型,至于内容,就可以自己定义了)
2、通过反射机制加载资源,而不是通过Context.getResource().getString()方式。
好了,介绍到此结束吧,稍后会把源代码打包,提供下载测试。demo存在bug,另外关于实现方式等问题,欢迎大家交流指正。
应用程序换肤,我介绍了两种方案,这两种方案都可以达到我们的目的。然而这两种方案都没有触及android资源管理框架的“灵魂”,如果巧妙的利用资源获取流程中的关键点,增加适配框架,能够让我们达到更换系统层的资源主题的目的。