前言
之前我们总结过B站的皮肤框架MagicaSakura
,也点出了其不足,文章链接:来自B站的开源的MagicaSakura源码解析,该框架只能完成普通的换色需求,没有QQ,网易云音乐类似的皮肤包的功能。
那么今天我们就来看一款拥有皮肤加载功能的插件化换肤框架。其已经集成在我的应用https://github.com/Jerey-Jobs/KeepGank中。
这样做有两个好处:
- 皮肤可以不集成在apk中,减小apk体积
- 动态化增加皮肤,灵活性大,自由度很大
如何实现换肤功能
想当然的,在View创建的时候这是让我们应用能够完美的加载皮肤的最好方案。
那么我们知道,对于Activity来说,有一个可以复写的方法叫onCreateView
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return super.onCreateView(parent, name, context, attrs);
}
我们的view的创建就是通过这个方法来的,我们甚至可以通过复写这个方法,实现view的替换,比如本来要的是TextView,我们直接给它替换成Button.而这个方法其实是实现的LayoutInflaterFactory
接口。
关于LayoutInflaterFactory
,我们可以看一下鸿神的文章http://www.tuicool.com/articles/EVzEny6
创建View
根据拿到的onCreateView
里面的name,来反射创建View,这边用到了一个技巧:onCreateView
中的name,对于系统的View,是没有'.'符号的,比如"TextView"我们拿到的直接是TextView,
但是自定义的View,我们拿到的是带有包名的全部名称,因此反射时,对于系统的View,我们需要加上系统的包名,自定义的View,则直接使用name。
也不用疑问为什么用反射,这样不是慢吗?
因为系统的LayoutInflater
在createView的时候也是这么做的,这边的代码都是参考系统的实现的。
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
static View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
// 系统控件,没有".",因此去创建系统View
if (-1 == name.indexOf('.')) {
// 根据名称反射创建
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
// 有'.'的情况下是自定义View,V4与V7也会走
} else {
// 直接根据名称创建View
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
/**
* 反射,使用View的两参数构造方法创建View
* @param context
* @param name
* @param prefix
* @return
* @throws ClassNotFoundException
* @throws InflateException
*/
private static View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
判断View是否需要换肤
与创建View一样,根据拿到的onCreateView
里面的AttributeSet attrs
拿到后,我们解析attrs
/**
* 拿到attrName和value
* 拿到的value是R.id
*/
String attrName = attrs.getAttributeName(i);//属性名
String attrValue = attrs.getAttributeValue(i);//属性值
根据属性名和属性值进行判断,有背景的属性,是否符合需要换肤的属性、
插件化资源注入
我们的皮肤包其实是APK,是我们写的另一个app,与正式App不同的是,其只有资源文件,且资源文件需要和主app同名。
1.通过 PackageManager拿皮肤包名
2.拿到皮肤包里面的Resource
但是因为我们想new Resources()
时候,发现其第一个参数是AssetManager
,但是AssetManager
的构造方法在源码中被@hide
了,我们没有方法拿到这个类,但是幸好其类还是能拿到的,我们直接反射获取。
我们拿资源的代码如下。
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
/**
* AssetManager assetManager = new AssetManager();
* 这个方法被@ hide了。。我们只能通过反射newInstance
*/
AssetManager assetManager = AssetManager.class.newInstance();
/**
* addAssetPath同样被系统给hide了
*/
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
/**
* 讲皮肤路径保存,并设置不是默认皮肤
*/
SkinConfig.saveSkinPath(context, params[0]);
skinPath = skinPkgPath;
isDefaultSkin = false;
/**
* 到此,我们拿到了外置皮肤包的资源
*/
return skinResource;
如何动态的从皮肤包中获取资源
我们以从皮肤包里面获取color来举例
业务端是通过资源的id来获取color的,资源的id也就是一个在编译时就生成的int型。 而皮肤包的也是编译时生成的,因此两个id是不一样的,我们只能通过资源的id先拿到在我们应用里的该id的名字,再通过名字去资源包里面拿资源。
public int getColor(int resId) {
int originColor = ContextCompat.getColor(context, resId);
/**
* 如果皮肤资源包不存在,直接加载
*/
if (mResources == null || isDefaultSkin) {
return originColor;
}
/**
* 每个皮肤包里面的id是不一样的,只能通过名字来拿,id值是不一样的。
* 1. 获取默认资源的名称
* 2. 根据名称从全局mResources里面获取值
* 3. 若获取到了,则获取颜色返回,若获取不到,老老实实使用原来的
*/
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
int trueColor;
if (trueResId == 0) {
trueColor = originColor;
} else {
trueColor = mResources.getColor(trueResId);
}
return trueColor;
}
实际使用
上面都是我们插件化加载的需要了解的知识,真的进行框架使用的时候,使用了自定义属性,根据自定义属性判断是否需要换肤。
使用观察者模式,所有需要换肤的view都会存放在Activity一个集合中,在皮肤管理器通知皮肤更新时,主动更新视图状态。
说了这么多了,框架的分装和使用具体可以看我的工程里面的代码。
https://github.com/Jerey-Jobs/KeepGank
效果如图:
细心的朋友会注意到,每个主题主页的左上角图片是会变的,没错,那个图片是动态加载的资源包里面的。
代码见:https://github.com/Jerey-Jobs/KeepGank
欢迎star
APK下载 App下载链接
本文作者:Anderson/Jerey_Jobs
博客地址 : http://jerey.cn/
简书地址 : Anderson大码渣
github地址 : https://github.com/Jerey-Jobs