目前的插件化正如火如荼,插件化开源的也不少,比如360开源的Replugin,滴滴的VirtualApk等等,当然我们今天的主题并不是插件化,而是插件化换肤;
android的换肤功能的实现基本有两种,一种是应用内换肤,一种是插件化换肤,应用内换肤比较简单,基本都是在内部预置几套皮肤,但是这样的话,一两套的皮肤来说还好,如果更多的话,会造成apk的体积非常大,很不好,如果我们能够动态的通过网络下载皮肤,动态的去更换,岂不是更好;插件化换肤,就能够满足这种使用场景,下面开始详细实现该方案;
想要插件话换肤,我们可以通过网络下载只有资源的apk资源包,虽然资源有了,但是我们有以下几个问题需要解决:
1,如何去获取插件资源?
2,如何找到需要资源的view?
3,我们该通过view的什么方法去设置资源?
对于第一个问题,由于是插件中的资源,插件中的资源id在宿主的R文件中是不存在的,我们该怎么找到对应插件中的资源呢。
其实这就需要我们定义一套规则了,我们可以规定插件资源名字必须和宿主中的资源名字一致,有了这个规则,那么我们可以根据资源的名字进行对应了: 宿主id —— 资源名字 —— 插件id
我们通过宿主的资源id值找到对应的资源名字:
getResources().getResourceEntryName(resId);
我们通过该方法就可以拿到资源的名字,这一步完成了通过宿主id找资源名字的过程
getResources().getResourceTypeName(resId);
通过该方法我们可以拿到资源的类型,是color还是drawable等等;
现在我们需要构造插件的Resources对象了,也很简单,通过Assetmanager的addAssetpath方法,不过需要反射,不懂该操作的可以自行百度原理,我就不详细深入分析了,直接上代码:
public class PluginResProvider {
public static String getPkg(Context context,String skinApkPath){
PackageInfo packageInfo = context.getPackageManager().getPackageArchiveInfo(skinApkPath, PackageManager.GET_UNINSTALLED_PACKAGES);
return packageInfo !=null? packageInfo.packageName : null;
}
public static Resources getResources(Context context,String skinApkPath){
AssetManager assetManager =getAssetManager(skinApkPath);
if (assetManager !=null) {
Resources hostRes = context.getResources();
return new Resources(assetManager,hostRes.getDisplayMetrics(),hostRes.getConfiguration());
}else {
return null;
}
}
public static AssetManager getAssetManager(String skinApkPath){
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinApkPath);
return assetManager;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
应该已经一目了然了吧,现在我们就可以通过资源名字拿到对应的插件资源的id了,通过插件的Resources的getIdentifier方法我们就拿到了插件中的资源id,这样我们就也可以在插件的Resources对象中通过插件的id来查找资源了。
接下来就是我们需要知道哪些view需要换肤的功能,当然这里分为两个方向,一个是对layout文件入手,一个是从代码层面入手,如果从layout入手,我们需要自己去给view定义一个换肤的标识属性,然后在给layoutInflater设置factory,在解析的时候去收集那些具有换肤tag的view,这种方法优点就是支持了在xml中的配置,减少了开发者代码层面的操作,缺点就是侵入性比较大,不光需要开发者在布局文件中去配置换肤属性,也需要进行注册,并且无法支持自定义属性的一些换肤操作,比如一个自定义view有个setTitleColor方法,显然这种方式扩展性比较差,灵活度也不高。另一种方式就是通过代码的方式,由开发者自己通过代码去设置哪个view需要换肤,需要什么方法换肤等等,忧点就是灵活度高,扩展性强,几乎零侵入,只需要开发者遵守资源名字一致即可,不需要再布局文件再去学习新的换肤标签属性,注册解注册很灵活,扩展性强,缺点就是需要代码量会多一些。
基于以上,所以我选择通过代码去实现换肤功能,为了能够支持自定义view的属性方法,能够扩展,所以我认为封装的时候不应该将换肤的具体方法写死,而是应该提供一个接口让开发者自己去实现,当然我们可以提供常用的方法就好,比如,setBackgorund,setBackgroundColor,setImageDrawable,setTextColor等等。还有就是,虽然本文的重点是换肤,但是我觉得我们不该局限于此,如果我想把插件中只要有id的资源都拿来用可不可以,比如,我们想用插件中的anim,string,array等等,我们不应该局限于换肤,它应该是一个可以拿到任何有id的资源,所以,我们也可以提供一个接口,让开发者有能力去扩展他想要的任何插件中的资源,我们只需要提供默认的即可,比如获取color,drawable等等;
这是我封装的包结构图,首先fetchers包下是资源抓取器,主要是实现了一些默认获取图片和颜色的资源抓取,如果想扩展可以直接继承ResFetcher类进行扩展:
public class ColorResFetcher extends ResFetcher {
@Override
protected Integer getRes(Resources pluginRes, int pluginId) {
return pluginRes.getColor(pluginId);
}
@Override
public String name() {
return FetcherType.COLOR;
}
}
这个是颜色的抓取,由于资源的类型不是随意的,所以,所有的资源类型我都预先定义在FetcherType类中:
@Retention(RetentionPolicy.SOURCE)
@StringDef({FetcherType.COLOR, FetcherType.DRAWABLE, FetcherType.STRING, FetcherType.ARRAY, FetcherType.ANIM, FetcherType.DIMEN, FetcherType.LAYOUT})
public @interface FetcherType {
String COLOR = "color", DRAWABLE = "drawable", STRING = "string",
ARRAY = "array", ANIM = "anim", DIMEN = "dimen", LAYOUT = "layout";
}
这里包含了很多资源类型名称,如果想要扩展,可以直接仿照ColorResFetcher去实现自己想要的资源。
public abstract class ResFetcher {
public T getRes(ResProxy resProxy, ViewAttr attr) {
T pluginRes = null;
int pluginId = resProxy.getCacheId(attr.hostResId);
if (pluginId == 0) {
pluginId = resProxy.getPulginId(attr);
}
try {
pluginRes = getRes(resProxy.getPluginRes(), pluginId);
} catch (Exception e) {
}
if (pluginRes != null) {
resProxy.saveId(attr.hostResId, pluginId);
} else {
}
return pluginRes;
}
protected abstract T getRes(Resources pluginRes, int pluginId);
public abstract @FetcherType
String name();
}
如果想要注册自己的资源抓取器,可以通过SkinCenter.get().addFetcher方法,下面看看SkinCenter这个类:
public class SkinCenter {
private static final String TAG = SkinCenter.class.getSimpleName();
private static SkinCenter skinCenter;
private WeakHashMap> cacheMap = new WeakHashMap<>();
private ResProxy mResProxy;
private SkinCenter() {
addDefaultFetchers();
addDefaultConverts();
}
@MainThread
public static SkinCenter get() {
if (skinCenter == null) {
skinCenter = new SkinCenter();
}
return skinCenter;
}
public void init(Context context) {
mResProxy = new ResProxy(context.getApplicationContext());
loadPlugin(context);
}
/**
* 添加默认的资源解析器
*/
private void addDefaultFetchers() {
addFetcher(new ColorResFetcher()).addFetcher(new DrawableResFetcher());
}
public SkinCenter addFetcher(ResFetcher fetcher) {
ResFetcherM.addFetcher(fetcher);
return this;
}
/**
* 添加默认的方法转换器
*/
private void addDefaultConverts() {
addConvert(new SetBackground())
.addConvert(new SetBackgroundColor())
.addConvert(new SetTextColor())
.addConvert(new SetImageDrawable());
}
public SkinCenter addConvert(IViewConvert convert) {
ConvertM.addConvert(convert);
return this;
}
/**
* 加载插件资源
* @param context
*/
private void loadPlugin(Context context) {
String skinPath = SkinCache.getCurSkinPath(context);
if (TextUtils.isEmpty(skinPath)){
loadDefaultSkin(context);
}else {
loadSkin(context,skinPath);
}
}
/**加载宿主默认的资源
* @param context
*/
public void loadDefaultSkin(Context context){
SkinCache.saveSkinPath(context,"");
if (mResProxy ==null){
mResProxy =new ResProxy(context.getApplicationContext());
}
mResProxy.setPlugin(context.getResources(),context.getPackageName());
notifySkinChanged();
}
/**提供外部调用加载插件资源
* @param context
* @param skinPath
*/
public void loadSkin(Context context,String skinPath){
if (TextUtils.isEmpty(skinPath) || !new File(skinPath).exists()){
return;
}
String pkg = PluginResProvider.getPkg(context, skinPath);
Resources pluginRes =PluginResProvider.getResources(context,skinPath);
if (pkg ==null || pluginRes ==null){
return;
}
SkinCache.saveSkinPath(context,skinPath);
if (mResProxy ==null){
mResProxy =new ResProxy(context.getApplicationContext());
}
mResProxy.setPlugin(pluginRes,pkg);
notifySkinChanged();
}
public void apply(V view, int resId, String convertName) {
apply(view, resId, ConvertM.getConvert(convertName));
}
public void apply(V view, int resId, IViewConvert convert) {
if (mResProxy == null) {
init(view.getContext());
}
applyAndRegist(view, resId, convert);
}
private void applyAndRegist(V view, int resId, IViewConvert convert) {
SparseArray attrs = cacheMap.get(view);
if (attrs != null) {
ViewAttr viewAttr = attrs.get(resId);
if (viewAttr != null) {
viewAttr.apply(view, mResProxy);
}
} else {
attrs = new SparseArray<>();
String idName = mResProxy.getResourceEntryName(resId);
String idType = mResProxy.getResourceTypeName(resId);
ResFetcher fetcher = ResFetcherM.getFetcher(idType);
if (idName != null && idType != null && fetcher != null) {
ViewAttr attr = new ViewAttr(resId, idName, idType, convert, fetcher);
attrs.put(resId, attr);
cacheMap.put(view, attrs);
attr.apply(view, mResProxy);
} else {
Log.e(TAG, "no match res,idName=" + idName + " ,idType=" + idType + " ,fetcher=" + fetcher);
}
}
}
private void notifySkinChanged() {
Iterator it = cacheMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
View view = (View) entry.getKey();
SparseArray attrs = (SparseArray) entry.getValue();
for (int i = 0; i < attrs.size(); i++) {
attrs.valueAt(i).apply(view, mResProxy);
}
}
}
}
这个类时对外提供的一个核心类,主要负责换肤view的注册,资源抓取器的注册,初始化等等,我们无需关心该怎么初始化,比如:
SkinCenter.get().apply(iv_image, R.color.colorAccent, SetBackgroundColor.class.getSimpleName());
一句代码就已经完成了换肤功能,iv_image是你要换肤的view,第二个参数是资源id,最后是方法转换器的类名,这个是默认已经写好的,在上面的类结构图里面已经列举了,如果SetBackgroundColor不能满足你,你可以自己实现IVewConvert接口,这个相当于方法转换器:
public class SetBackgroundColor implements IViewConvert {
@Override
public void apply(View view, Integer res) {
view.setBackgroundColor(res);
}
}
这个就是默认提供的,你可以自己去定义自己的方法逻辑;
当然如果你想要换肤,有了apk的路径,那么你只需要这样做:
SkinCenter.get().loadSkin(context,skinPath);
一句话即可换肤,所有需要的换肤的view都会自动换肤,是不是很简单。
有不足之处,欢迎大家提出,下面是源码地址:
https://github.com/wangzhanxian/SkinClient