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应用程序的Context
				return 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 = 01
id = 02
Style = 03
String = 04
dimmen = 05
color = 06
array = 07
drawable = 08
layout = 09
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资源管理框架的“灵魂”,如果巧妙的利用资源获取流程中的关键点,增加适配框架,能够让我们达到更换系统层的资源主题的目的。

程序源代码下载



关注我的技术公众号,查看更多优质技术文章推送

微信扫一扫下方二维码即可关注:

关注我的技术公众号,查看更多优质技术文章推送


  • 6
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hailushijie

您的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值