一.原理
动态换肤是将多种资源文件放在皮肤包中,皮肤包本质上就是打包成的APK文件,与静态换肤相比,动态换肤将皮肤资源分离出来单独打包,可以有效减少APP的大小。下图是APK文件的内部组成:
其中classes.dex文件中的内容对应的是Java代码,在皮肤包中这部分内容是不需要的。resources.arsc文件中的内容是资源文件,如下图所示:
每一个资源文件都有一个ID,如“0x7f040026”,其中,“0x7f”是标准规范,每个资源ID都以它开头;“04”代表color类别,比如drawable为“06”、layout为“0a”;“0026”代表序号,资源顺序是按字母进行排序的。
动态换肤的基本原理是:
- APP中需要替换的颜色、图片等资源和皮肤包中的资源是一一对应的,具有相同的文件名。
- 在APP中,可以拿到需要替换的资源文件名和类型,根据资源文件名和类型可以去皮肤包中查找这个资源对应的ID。
- 根据资源ID可以加载出皮肤包中的资源,从而替换APP中的资源文件。
二.实现
1. SkinManager类
首先新建一个皮肤管理类SkinManager类,该类首先要分别得到APP和皮肤包的Resources对象。APP的Resources对象可以通过Application.getResources()获得,皮肤包的Resources对象获取方法如下所示。
/**
* 加载皮肤包资源
* @param skinPath 皮肤包路径,为空则加载app内置资源
*/
public void loaderSkinResources(String skinPath) {
// 如果没有皮肤包或者没做换肤动作,方法不执行直接返回!
if (TextUtils.isEmpty(skinPath)) {
isDefaultSkin = true;
return;
}
try {
// 通过反射创建资源管理器(用来加载外部APk,不能用Application.getAssets())
AssetManager assetManager = AssetManager.class.newInstance();
// AssetManager中的addAssetPath方法只能通过反射去执行方法
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
// 设置私有方法可访问
addAssetPath.setAccessible(true);
// 执行addAssetPath方法
addAssetPath.invoke(assetManager, skinPath);
// 创建加载外部的皮肤包文件Resources
skinResources = new Resources(assetManager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
// 根据apk文件路径,获取包名。
skinPackageName = application.getPackageManager()
.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
// 如果获取包名失败,则加载app内置资源
isDefaultSkin = TextUtils.isEmpty(skinPackageName);
if (!isDefaultSkin) {
cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
}
} catch (Exception e) {
e.printStackTrace();
isDefaultSkin = true;
}
}
得到皮肤包的Resources对象后,还需要有一个方法用于查找与APP资源对应的皮肤包资源ID。
private int getSkinResourceIds(int resourceId) {
//如果使用默认皮肤,直接返回app内置资源!
if (isDefaultSkin) return resourceId;
// 根据APP内置资源获得资源名称和类型(如“mojave”, “drawable”)
String resourceName = appResources.getResourceEntryName(resourceId);
String resourceType = appResources.getResourceTypeName(resourceId);
// 根据资源名称和类型去皮肤包中查找对应的资源ID
int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);
isDefaultSkin = skinResourceId == 0;
return skinResourceId == 0 ? resourceId : skinResourceId;
}
得到皮肤包的资源ID后,就可以根据该资源ID找到资源文件,并设置到View中。
2.自定义View
与静态换肤相同,在第一次加载布局时,通过设置自定义的Factory2对象,在onCreateView方法中用自定义的View去代替系统View,在自定义View中进行资源替换工作。自定义View均实现ViewChange接口,在skinChange中进行换肤操作。与静态换肤的区别就是资源是在外部皮肤包中加载的。部分代码如下:
// 根据自定义属性,获取styleable中的textColor属性
key = R.styleable.SkinnableButton[R.styleable.SkinnableButton_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
//如果是默认皮肤,则使用APP内置资源
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
setTextColor(color);
} else {
//如果选择了换肤,则使用皮肤包资源
ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
setTextColor(color);
}
}
换肤效果如图所示: