Andorid的插件化换肤的思路,首先我们要弄清楚换肤的原理,从源码上下手,弄清原理,不过在看源码之前,我还是先说说换肤的思路,然后我们带着疑问去看看源码;思路如下:
换肤,换肤,换的是View对应的资源,如TextView更换字体的颜色,换的是color这个资源,我们要拿到这个资源ID;那插件化换肤的两个很重要的思路,
- 我们先拿到对应资源的string名称,我们的皮肤包中也同样存在这样一个同名的资源,我们使用这个string资源名称去拿皮皮肤包中的同名资源的资源ID,然后根据皮肤包的资源ID加载相应的资源;
- 在我们找了相应的资源ID,这个时候需要把资源加载到相应的View上面去,我们如何拿到所有的view,并且记录所需要更换的View的属性,如TextView我们可能需要更换的属性就有textColor,ImageView有src(更换显示的图片 ),关于如何拿到所有的View,我们可以使用LayoutInflater.Factory2这个接口,我们需要侵入,系统中创建View的这个过程,记录view,及其相应的View所需要更换的资源属性;(这个地方可以考虑是否有更好的实现方式);
先看看这个第二个思路,关于view的创建,view是如何创建view的;
上面的这个函数最终会进入下面这个函数:
在这里提一嘴,我们平时调用上面这个函数的时候,一般都会attachToRoot这个参数设置为false,为什么要设置为false,如果我们设置为true,会发现我们要创建的布局,根TAG有些属性不会生效,先看如下代码:
先获取root的LayoutParams,如果attachToRoot为false的话,那么就将这个LayoutParams设置到view上面去,否则的话,不设置LayoutParams,从而view设置的一些布局参数才会生效,view被创建出来的时候,并不会设置相应的属性,从而我们attachToRoot为true的时候,view的布局参数不会生效,再回到问题中来;
上面的这个函数是解析layout.xml文件的关键函数,会创建View,并为view设置相应的参数,里面核心的函数如下:
可以看到createViewFromTag函数中,首先调用的是tryCreateView这个函数,从函数的字面意义上理解,先尝试创建view,然后调用系统的onCreateView或者createView方法;
而在tryCreateView这个方法中,我们可以看到这样的一个逻辑如果mFactory2不为空,那么先使用mFactory2中的onCreateView方法创建view,而这个mFactory2是个什么呢?秘诀就在这里,先看看mFactory2的结构,如下:
这是一个接口,我们可以实现这个接口然后介入到创建view的这个一个过程中去,记录所有的view,同时拿到view所有需要换肤的属性,记录下来;
接下来我们先实现这个接口,直接上代码了:
@Nullable
@Override
public View onCreateView(@Nullable View parent,
@NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
//创建系统的
View view = createSDKView(name, context, attrs);
if (view == null) {
view = createView(name, context, attrs);
}
if (view != null) {
//将这个View中的那些属性可以替换做一个记录
skinAttribute.look(view, attrs);
}
return view;
}
/**
* 根据系统view的名字创建系统提供的 View实例
* 创建系统的View的时候 需要添加View的前缀,因为一般系统的View都是只有View的名字
*
* @param name
* @param context
* @param attrs
* @return
*/
private View createSDKView(String name, Context context, AttributeSet attrs) {
//TODO 如果包含点(.)的话 那么这个view标签就是jar包或者自定义View的
if (name.indexOf('.') != -1) {
//这个时候可以我们自己去创建
return null;
}
for (String viewName : mClassPrefixList) {
View view = createView(viewName + name, context, attrs);
if (view != null) {
return view;
}
}
return null;
}
public Constructor<? extends View> findConstructor(String name, Context context) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor == null) {
Class<? extends View> clazz = null;
try {
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return constructor;
}
/**
* TODO 根据view的名字创建View对象
*
* @param name view的名字
* @param context 上下文
* @param attributeSet view的构造函数的第二个函数
* @return view 实例
*/
public View createView(String name, Context context, AttributeSet attributeSet) {
Constructor<? extends View> constructor = findConstructor(name, context);
try {
return constructor.newInstance(context, attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
以上的其中我们在创建view的时候就是使用反射来创建对应view的实例,其中注意的是,view的名字如果带“.”符号的话,那么就是全限定名,一般就是Jar包中或者自定义的view会使用全限定名,这个时候可以直接使用反射获取view的构造方法,如果没有全限定名,那么就需要我们自己加上前缀,然后组合成全限定名,然后获取构造方法创建view的实例;
然后look方法中,代码如下:
我们需要记录view对应的属性名和对应的属性资源ID,这个资源ID取@符号后面的字符串,在转化为数字,然后创建SkinPair类记录下来;这里的思路是这样的,我们记录下来,我们需要置换资源view的属性名,和对应的资源ID,在我们需要进行更换的时候,循环这个列表,然后找到皮肤包中的资源ID替换我们宿主App的ID;
然后最后一个难点,关于如何拿到这个皮肤包中的资源ID呢?我们首先需要拿到皮肤包中对应的Resources对象,代码如下所示:
以上使用了反射构建了AssetManager对象;然后使用这个皮肤包中的Resources对象,我们可以找到对应的皮肤包中的同名资源ID,代码如下:
以上的代码根据宿主APP中的资源name,获取皮肤包中的资源ID;然后我们需要换肤的时候,可以根据我们之前拿到的所有的view的属性和其资源ID来更换为皮肤包中的资源ID;达到换肤的效果;代码如下: