Android应用程序内换肤解决方案(一)之测试Demo

转载请注明出处: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应用程序资源是如何读取的:

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

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

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

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

[java]  view plain copy
  1. public abstract class Context {  
  2.     ......  
  3.     /** 
  4.      * Return a new Context object for the given application name.  This 
  5.      * Context is the same as what the named application gets when it is 
  6.      * launched, containing the same resources and class loader.  Each call to 
  7.      * this method returns a new instance of a Context object; Context objects 
  8.      * are not shared, however they share common state (Resources, ClassLoader, 
  9.      * etc) so the Context instance itself is fairly lightweight. 
  10.      * 
  11.      * <p>Throws {@link PackageManager.NameNotFoundException} if there is no 
  12.      * application with the given package name. 
  13.      * 
  14.      * <p>Throws {@link java.lang.SecurityException} if the Context requested 
  15.      * can not be loaded into the caller's process for security reasons (see 
  16.      * {@link #CONTEXT_INCLUDE_CODE} for more information}. 
  17.      * 
  18.      * @param packageName Name of the application's package. 
  19.      * @param flags Option flags, one of {@link #CONTEXT_INCLUDE_CODE} 
  20.      *              or {@link #CONTEXT_IGNORE_SECURITY}. 
  21.      * 
  22.      * @return A Context for the application. 
  23.      * 
  24.      * @throws java.lang.SecurityException 
  25.      * @throws PackageManager.NameNotFoundException if there is no application with 
  26.      * the given package name 
  27.      */  
  28.     public abstract Context createPackageContext(String packageName,  
  29.             int flags) throws PackageManager.NameNotFoundException;  
  30.     ......  
  31. }  
Context对象在它的很多的子类中提供了这个方法,这个方法允许我们通过应用程序的packageName来创建外部应用的Context对象。

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

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

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

[java]  view plain copy
  1. private ArrayList<ThemeNameAndPackageName> getPackageInstallName(){  
  2.         Log.d(LOG_TAG, "getPackageInstallName...");  
  3.         ArrayList<ThemeNameAndPackageName> arrayList = new ArrayList<ThemeNameAndPackageName>();  
  4.         List<PackageInfo> mPackageInfo = mPackageM.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);//得到系统中所有的安装应用信息,这个方法,flag参数传递不好,导致Binder传递太多数据,引出bug,这个flag也不好  
  5.         for(PackageInfo info : mPackageInfo){//遍历所有应用,找到符合规则的packageName,加入到List中。  
  6.             if(info.packageName!=null&&info.packageName.startsWith(mPackageName)){  
  7.                 ThemeNameAndPackageName item = new ThemeNameAndPackageName();  
  8.                 String name = null;  
  9.                 if(info.packageName.equals(mFreFragment.getActionPackageName())){  
  10.                     mSelectedThemeAndPackageName = item;  
  11.                 }  
  12.                 if(info.packageName.equals(mPackageName)){  
  13.                     name = SettingsTheme.this.getString(R.string.theme_local);  
  14.                 }else{  
  15.                     try {  
  16.                         Context packageContext =SettingsTheme.this.createPackageContext(info.packageName, Context.CONTEXT_IGNORE_SECURITY);  
  17.                         name = packageContext.getString(info.applicationInfo.labelRes);  
  18.                     } catch (NameNotFoundException e) {  
  19.                         name = SettingsTheme.this.getString(R.string.theme_outer);  
  20.                         e.printStackTrace();  
  21.                     }  
  22.                 }  
  23.                 item.setName(name);  
  24.                 item.setPackageName(info.packageName);  
  25.                   
  26.                 Log.d(LOG_TAG, "packageInfo toString is:"+item);  
  27.   
  28.                 arrayList.add(item);  
  29.             }  
  30.         }  
  31.         return arrayList;  
  32.     }  
什么样的主题应用才是我们能够识别的那?

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

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

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

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

[java]  view plain copy
  1. private Context createRightContext(){  
  2.         Context rightContext = mContext;  
  3.         String packageName = Utils.getPersistPackageContextName(mContext);  
  4.         if(DEBUG)Log.d(LOG_TAG, "ResourceConfig packageName is:"+packageName+",,,Context packageName is:"+mContext.getPackageName());  
  5.         if(packageName != null){  
  6.             try {  
  7.                 rightContext = rightContext.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY);//见2中介绍,创建外部apk应用程序的Context  
  8.                 return rightContext;  
  9.             } catch (NameNotFoundException e) {  
  10.                 e.printStackTrace();  
  11.                 if(DEBUG)Log.d(LOG_TAG, "packageName '"+packageName+"' create context fail!!!");  
  12.             }  
  13.         }  
  14.         return rightContext;  
  15.     }  
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文件。

[java]  view plain copy
  1. public final class R {  
  2.     public static final class attr {  
  3.     }  
  4.     public static final class drawable {  
  5.         public static final int bj=0x7f020000;  
  6.         public static final int btn=0x7f020001;  
  7.         public static final int btn_switch_normal=0x7f020002;  
  8.         public static final int btn_switch_pressed=0x7f020003;  
  9.         public static final int button_switcher_drawable=0x7f020004;  
  10.         public static final int green_divider=0x7f020005;  
  11.         public static final int ic_launcher=0x7f020006;  
  12.     }  
  13.     public static final class id {  
  14.         public static final int btn04=0x7f050003;  
  15.         public static final int btn_comfirm=0x7f050001;  
  16.         public static final int fre_view=0x7f050000;  
  17.         public static final int root=0x7f050002;  
  18.     }  
  19.     public static final class layout {  
  20.         public static final int af=0x7f030000;  
  21.         public static final int main=0x7f030001;  
  22.     }  
  23.     public static final class string {  
  24.         public static final int app_label=0x7f040000;  
  25.         public static final int app_name=0x7f040001;  
  26.         public static final int button_show_style=0x7f040002;  
  27.         public static final int button_theme_confirm=0x7f040003;  
  28.         public static final int list_empty=0x7f040008;  
  29.         public static final int theme_fre_view=0x7f040004;  
  30.         public static final int theme_local=0x7f040006;  
  31.         public static final int theme_outer=0x7f040007;  
  32.         public static final int theme_settings=0x7f040005;  
  33.     }  
  34. }  

[java]  view plain copy
  1. <string name="app_label">换肤解决方案(二)</string>  

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

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

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

[java]  view plain copy
  1. 问题:(本人叙述能力不强,问题描述有点罗嗦,见谅!!!)  
  2. 我们通过Context.getResource().getString(R.String.app_label);得出app_label对应的字符串的值,也就是“换肤解决方案(二)”   见上文 附。  
  3. 换句话说:Context.getResource().getString(0x7f040000) ==  “换肤解决方案(二)”  
  4. 我们此时的Context是我们主应用程序本身,可如果我们读取的是外部的apk应用程序,那么Context就是外部apk应用程序的Context,  
  5. 问题来了,假如外部应用程序存在<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),后面的四位代表资源编号。

资源类型:

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

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

解决方案:

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


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

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

程序源代码下载



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值