插件化换肤技术,就是通过插件的方式,加载外部资源文件,无需更新App就可以实现换肤,具有耦合低,入侵小的特点。总结起来就是。
- 更好的用户体验,无闪烁换肤架构
- 扩展和维护方便,入侵性小,低耦合
- 插件化开发,任何APP都是你的皮肤包
- 立即生效,无需要重启APP
github项目地址 https://github.com/Rainpler/EnjoySkin
为实现插件化换肤的技术,需要先了解布局原理与资源加载原理,这些都在上一篇中进行了分析。整个插件化框架共分为三个部分,一个是主App,一个是换肤框架,另一个是资源包apk。
在资源加载原理中我们分析到,当ResourcesImpl创建完成后,又会接着调用getOrCreateResourcesLocked()去初始化Resources对象实例。最终将包装好的resources作为资源类返回,资源的信息都被存储在Resources中的ResourcesImpl中的Asset对象中。
所以实际上,Resource和ResourceImpl都是包装的壳,最终资源的读取都是通过assets来进行的。而此外,还有这些重要的API:
/*package*/ native final int getResourceIdentifier(String name,String defType,String defPackage);
/*package*/ native final String getResourceName(int resid);
/*package*/ native final String getResourcePackageName(int resid);
/*package*/ native final String getResourceTypeName(int resid);
/*package*/ native final String getResourceEntryName(int resid);
所以,我们就可以利用资源映射,通过主App中的资源id,寻找到其在资源apk对应的资源,并动态替换。本文直接从实战出发,讲解插件化换肤技术的基本流程。共分为以下五个步骤:
- 制作皮肤包
- 收集XML数据
- 统计换肤需要的属性
- 读取皮肤包资源
- 执行换肤
制作皮肤包
皮肤包其实就是一个apk文件,我们可以新建一个工程,将除了资源文件外的其他文件删除,然后build生成apk,就可以得到我们所需要的apk了。需要注意的是,该皮肤包中的资源文件命名必须与主工程中待替换的资源文件命名一致,这样才可以通过资源映射找到相关文件。
这样一来,不同的皮肤我们就可以打包成不同的apk,当执行换肤的时候就可以根据要换肤的apk,映射到相关的资源文件中。
收集XML数据
我们执行换肤操作的时候,需要先统计,在布局原理中我们讲到,view的创建过程中,如果factory不为空,则view的创建会被拦截,所以我们利用view生产对象的过程中的Factory2接口,自定义SkinLayoutInflateFactory。
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Factory2中有一个抽象方法onCreateView(),在view的创建时会调用该方法,所以我们可以直接沿用LayoutInflater源码中的实现,只需要在适当的位置添加我们自己的逻辑就可以了,在这里,我们需要统计每一个view需要换肤的属性。
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//换肤就是在需要时候替换 View的属性(src、background等)
//所以这里创建 View,从而修改View属性
View view = createSDKView(name, context, attrs);
if (null == view) {
view = createView(name, context, attrs);
}
//这就是我们加入的逻辑
if (null != view) {
//加载属性
skinAttribute.look(view, attrs);
}
return view;
}
统计需要换肤的属性
为了统计需要换肤的属性,我们定义了一个SkinAttribute的类,包含两个重要的属性。
- SkinPair 记录一个属性 属性名字——对应的资源id
- SkinView 一个view对应多个属性 List<SkinPair>
而SkinPair是其一个内部类,用于记录一对属性的名称与id。
- attributeName 属性名
- int resId 对应的资源id
SkinView也是其一个内部类,用于记录一个view对应的多个属性SkinPair
- view view对象
- List<SkinPair> 对应的属性列表
look方法实现如下:
//记录下一个VIEW身上哪几个属性需要换肤textColor/src
public void look(View view, AttributeSet attrs) {
List<SkinPair> mSkinPars = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//获得属性名 textColor/background
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
// #
// ?andorid:
// @drawable/xxx.png
String attributeValue = attrs.getAttributeValue(i);
// 比如color 以#开头表示写死的颜色 不可用于换肤
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
// 以 ?开头的表示使用 属性
if (attributeValue.startsWith("?")