一般小公司是不会接触到换肤的,尤其是动态下载皮肤插件来实现换肤。那么,今天我们来一探究竟。如何实现加载从网上下载的皮肤插件,并且替换到相应的控件中!
大致涉及到4个步骤:
1、下载皮肤插件(通常为apk,后期用skin.apk来表示皮肤插件)到本地
2、根据皮肤插件skin.apk的绝对路径,加载插件里的资源文件
3、准确找到需要换肤的控件
4、应用skin.apk里的资源,修改相应的属性
第一步下载皮肤插件就是简单地下载文件操作,这里就不做详细介绍,重点部分已经用红色加粗字体标记出来了!下面介绍一下关键的api,最后完整代码链接。
public void setSkinPath(String skinPath) {
this.skinPath = skinPath;
//获取包管理类
PackageManager packageManager = context.getPackageManager();
//获取插件包信息类
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
//获取插件包名
packageName = packageInfo.packageName;
//获取插件的资源文件
AssetManager assets = null;
try {
assets = AssetManager.class.newInstance();
Method method = assets.getClass().getMethod("addAssetPath", String.class);//方法名,参数类型
method.invoke(assets, packageName);//调用该方法的对象,传入的参数
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
resources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
如果包含src,textColor,background等等属性,就认为该控件需要换肤,这个是根据项目的需求而定,没有强制要求的。下面三个步骤分别对应三段关键代码:
//监听xml文件实例化View过程
skinFactory = new SkinLayoutInflateFactory();
LayoutInflaterCompat.setFactory2(getLayoutInflater(), skinFactory);
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {
//...
//可以取得每个View名字,通过每个view的名字,就可以动态实例化(实例化不会造成双重实例化,如果设置了监听,并且返回view的话,就不会掉用到系统的实例化)
/**
* 一个完整的类名,生成一个
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
try {
Class aClass = context.getClassLoader().loadClass(name);
Constructor extends View> constructor = aClass.getConstructor(new Class[]{Context.class, AttributeSet.class});
view = constructor.newInstance(context, attrs);
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
}
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {
//...
//实例化之后就可以得到相应的属性,通过AttributeSet获取到该View所有的属性,例如id,backgroundColor,@xxx(例如color)/xxx(例如white),获取到了信息其实就是对应R.xxx.xxx(例如R.color.white),根据这个名字去找到皮肤资源文件对应名字的资源,如果存在,那就替换,否则就不替换!
/**
* 如果控件已经实例化,那么我们就去判断这个控件是否符合换肤的需求
* @param view
* @param name
* @param attrs
*/
private void parseView(View view, String name, AttributeSet attrs) {
List listSkinItem = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);
String idStr = attrs.getAttributeValue(i);
if (attrName.contains("background")
||attrName.contains("textColor")
||attrName.contains("src")
||attrName.contains("color")
) {
//获取属性值id
int id = Integer.parseInt(idStr.substring(1));
String entryType = view.getResources().getResourceTypeName(id);
String entryName = view.getResources().getResourceEntryName(id);
SkinItem skinItem = new SkinItem(attrName, id, entryName, entryType);
listSkinItem.add(skinItem);
}
SkinView skinView = new SkinView(view, listSkinItem);
listWillBeChangeSkinView.add(skinView);
//skinView.apply();
}
SkinManager.getInstance().applyNewSkin(listWillBeChangeSkinView);
}
//...
}
前面一步拿到皮肤插件中对应名字资源的id,等等信息。通过id,利用皮肤插件的resource去查找这个id,如果有,就设置新的,没有就设置原来的。
public int getColor(int id) {
if(resources == null) return id;
//根据id获取属性值的名字
String entryName = context.getResources().getResourceEntryName(id);
//根据id获取属性值的类型
String entryType = context.getResources().getResourceTypeName(id);
//根据属性值的名称、属性值的类型、包名,查看该包名下这个属性的id
int newId = resources.getIdentifier(entryName, entryType, packageName);
if (newId == 0) {
return id;
}
return resources.getColor(newId);
}
初学者积分用得比较快,为了帮助大家省点,项目我就放到github给你们吧,喜欢的朋友高抬贵手给我一个star,谢谢!
源码传送门:https://github.com/KubyWong/JetpackSample
下面再来简单总结一下整体流程,
加载apk外部皮肤插件,获取的resource操作资源对象(apk路径->包名->AssetsManager->Resource,类的动态加载newInstance,动态载入方法invoke)
重写系统实例化xml文件中的view的过程(setFactory2()方法设置一个接口,复写方法即可)
拿到view实例中的各种属性,如id,属性类型type,值的类型valuetype,值的名字valuename(涉及到resource和attrbuteset操作)
替换view中各种属性的值(涉及到根据id拿到属性名,属性类型context.getResources().getResourceTypeName(id)
;根据属性名,属性类型,包名,拿到相应的包名下单资源id,关键代码resources.getIdentifier(entryName, entryType, packageName),)