一.原理
动态换肤是将多种资源文件放在皮肤包中,皮肤包本质上就是打包成的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对象获取方法如下所示。
public void loaderSkinResources(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
isDefaultSkin = true;
return;
}
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, skinPath);
skinResources = new Resources(assetManager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
skinPackageName = application.getPackageManager()
.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
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) {
if (isDefaultSkin) return resourceId;
String resourceName = appResources.getResourceEntryName(resourceId);
String resourceType = appResources.getResourceTypeName(resourceId);
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中进行换肤操作。与静态换肤的区别就是资源是在外部皮肤包中加载的。部分代码如下:
key = R.styleable.SkinnableButton[R.styleable.SkinnableButton_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
setTextColor(color);
} else {
ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
setTextColor(color);
}
}
换肤效果如图所示:
