本文章是在内置换肤的基础上进行扩充,内置换肤代码和原理讲解地址:内置换肤原理。如果没有查看这一章节内容,请跳过,本文绝大部分代码是内置换肤代码的扩展。这里只讲解核心代码。
一、动态换肤原理
首先需要明白resources.arsc资源映射表。打包一个apk,点击查看apk,包含如下内容:
这里可以看到resources.arsc,点击查看,如下截图:
左边一栏是资源类型type,右边包含资源id和资源名称name。三者对应关系如下:
- 不同的资源类型和资源名称会对应不同的资源id
- 相同的资源类型和资源名称对应相同的资源id
- 只要资源类型和资源名称有一个不相同,资源id就不同
动态换肤其实也就是根据三者的关系进行了"偷梁换柱",比如我们app中内置有一张图片,对应的资源id为0x7f060000,那么可以根据这个resourceId,获取到图片名称为img,类型为drawable。它是如何匹配皮肤包的相关资源呢?首先,你的皮肤包中必须有相同的图片名称和图片类型,根据图片名称和类型就可以获取到皮肤包的资源id,拿到这个资源id就可以设置图片了。
二、皮肤包
皮肤包其实就是一个apk,只是这个apk只需要配置res目录下的内容和字体样式。需要注意以下几点:
- colors.xml:里面的颜色资源要和app名称对应一致
- strings.xml:需要配置字体路径,名称要和app对应一致
以下为截图对比:
配置完成以后,只需要生成一下皮肤包apk即可。然后将生成的apk名称修改,这里我改成了qb.skin。然后将它放到SD卡即可。
配置皮肤包需要记住一下几点:
- 打出来的皮肤包可删除classes.dex,但至少保留:AndroidManifest.xml、resources.arsc、res/xxx。应该使用解压软件打开删除不需要的即可,不能先解压,删除后再进行压缩。
- app内置资源id和皮肤包的资源id不是一定一致的。比如app的drawable中有a.png,b.png,换肤1.png,换肤2.png,对应的资源id分别为0x7f060001,0x7f060002,0x7f060003,0x7f060004。我们皮肤包的drawable中有换肤1.png,换肤2.png,但是它对应资源id分别为0x7f060001,0x7f060002。
三、资源加载
1、初始化app资源appResource、皮肤包资源skinResource,皮肤包包名skinPackageName。主要代码如下:
private SkinManager(Application application) {
mApplication = application;
//获取当前app的资源
appResources = application.getResources();
skinCaches = new HashMap<>();
}
/**
* 1、获取皮肤包资源 skinResources
* 2、获取皮肤包包名 skinPackageName
*
* @param skinPath 皮肤包本地路径
*/
public void loadSkinResources(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
Log.e("SkinManager>>>>>>>>>", "加载本地资源");
isDefault = true;
return;
}
if (loadSkinCaches(skinPath)) {
isDefault = false;
Log.e("SkinManager>>>>>>>>>", "缓存找到");
return;
}
/*
获取当前app资源管理器 为什么不能使用mApplication.getAssets()
因为没有这种方式并没有初始化该类,是没法应用方法反射的
*/
//AssetManager assetManager = mApplication.getAssets();
try {
AssetManager assetManager = AssetManager.class.newInstance();
//获取当前要反射方法
Method method = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
method.setAccessible(true);
//执行反射,将皮肤包skin资源加载到当前app资源中
method.invoke(assetManager, skinPath);
//创建加载皮肤包资源,依然在app应用中加载
skinResources = new Resources(assetManager, appResources.getDisplayMetrics(),
appResources.getConfiguration());
//获取皮肤包包名
skinPackageName = mApplication.getPackageManager().getPackageArchiveInfo(skinPath,
PackageManager.GET_ACTIVITIES).packageName;
//加载成功,设置标识为false,表示可以加载皮肤包资源
if (!TextUtils.isEmpty(skinPackageName)) {
isDefault = false;
//保存到缓存中
skinCaches.put(skinPath, new SkinCache(skinPackageName, skinResources));
}
} catch (Exception e) {
e.printStackTrace();
isDefault = true;
}
}
2、根据资源app资源id加载皮肤包资源id。主要代码如下:
**
* 获取皮肤包资源id 参考resource.arsc资源映射表
*
* @param resourceId app的资源id
* @return 如果找到返回皮肤包资源id,反之返回app的resourceId
*/
public int loadResouceId(int resourceId) {
//没有皮肤包 直接返回app资源id
if (isDefault) return resourceId;
//获取app资源名称,比如ic_launcher.png
String resourceName = appResources.getResourceEntryName(resourceId);
//获取app资源类型 比如drawable
String resourceType = appResources.getResourceTypeName(resourceId);
/*
加载皮肤包的资源id 失败返回0
*/
int skinResourceId = skinResources.getIdentifier(resourceName, resourceType,
skinPackageName);
//失败isDefault = true
isDefault = skinResourceId == 0;
return isDefault ? resourceId : skinResourceId;
}
四、使用
:1、在需要换肤的activity中调用如下方法:
SkinManager.getInstance().loadSkinResources(skinPath);
2、skinPath为皮肤包的路径,通过以下方式获取:
//加载皮肤包
skinPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "qb.skin";
3、执行换肤的时候,比如TextView控件,在SkinTextView.class中的换肤方法中,修改之前代码如下:
public void viewChange() {
//改变控件背景颜色
int bgKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_background];
int bgColor = skinBean.getResource(bgKey);
//动态改变背景颜色
if (bgColor > 0) {
//是否加载皮肤包成功
if(SkinManager.getInstance().isDefault()){
// 兼容包转换
Drawable drawable = ContextCompat.getDrawable(getContext(), bgColor);
// 控件自带api,这里不用setBackgroundColor()因为在9.0测试不通过
// setBackgroundDrawable本来过时了,但是兼容包重写了方法
setBackgroundDrawable(drawable);
}else{
Object skinResourceId = SkinManager.getInstance().getBackgroundOrSrc(bgColor);
// 兼容包转换
if (skinResourceId instanceof Integer) {
int color = (int) skinResourceId;
setBackgroundColor(color);
// setBackgroundResource(color); // 未做兼容测试
} else {
Drawable drawable = (Drawable) skinResourceId;
setBackgroundDrawable(drawable);
}
}
}
int textColorKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_textColor];
int textColor = skinBean.getResource(textColorKey);
if (textColor > 0) {
//是否加载皮肤包成功
if(SkinManager.getInstance().isDefault()){
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColor);
this.setTextColor(color);
}else{
ColorStateList skinColor = SkinManager.getInstance().getColorStateList(textColor);
this.setTextColor(skinColor);
}
}
//字体样式
// 根据自定义属性,获取styleable中的字体 custom_typeface 属性
int typefaceKey = R.styleable.SkinTextView[R.styleable.SkinTextView_custom_typeface];
int textTypefaceResourceId = skinBean.getResource(typefaceKey);
if (textTypefaceResourceId > 0) {
//是否加载皮肤包成功
if (SkinManager.getInstance().isDefault()) {
setTypeface(Typeface.DEFAULT);
} else {
setTypeface(SkinManager.getInstance().getTypeface(textTypefaceResourceId));
}
}
}
其他自定义控件类似相关修改。
4、注意不要忘记在app的配置文件中加上权限
<!--皮肤包权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
最后上一波效果图,字体,图片和自定义控件都换了。是不是6666!!!
动态换肤代码地址如下:
代码地址:app动态换肤