前言
之前我們總結(jié)過(guò)B站的皮膚框架MagicaSakura
乳幸,也點(diǎn)出了其不足怕品,文章鏈接:來(lái)自B站的開源的MagicaSakura源碼解析治笨,該框架只能完成普通的換色需求考婴,沒(méi)有QQ砌们,網(wǎng)易云音樂(lè)類似的皮膚包的功能杆麸。
那么今天我們就來(lái)看一款擁有皮膚加載功能的插件化換膚框架。其已經(jīng)集成在我的應(yīng)用https://github.com/Jerey-Jobs/KeepGank中浪感。
這樣做有兩個(gè)好處:
- 皮膚可以不集成在apk中昔头,減小apk體積
- 動(dòng)態(tài)化增加皮膚,靈活性大影兽,自由度很大
如何實(shí)現(xiàn)換膚功能
想當(dāng)然的揭斧,在View創(chuàng)建的時(shí)候這是讓我們應(yīng)用能夠完美的加載皮膚的最好方案。
那么我們知道峻堰,對(duì)于Activity來(lái)說(shuō)讹开,有一個(gè)可以復(fù)寫的方法叫onCreateView
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return super.onCreateView(parent, name, context, attrs);
}
我們的view的創(chuàng)建就是通過(guò)這個(gè)方法來(lái)的,我們甚至可以通過(guò)復(fù)寫這個(gè)方法捐名,實(shí)現(xiàn)view的替換旦万,比如本來(lái)要的是TextView,我們直接給它替換成Button.而這個(gè)方法其實(shí)是實(shí)現(xiàn)的LayoutInflaterFactory
接口镶蹋。
關(guān)于LayoutInflaterFactory
成艘,我們可以看一下鴻神的文章http://www.tuicool.com/articles/EVzEny6
創(chuàng)建View
根據(jù)拿到的onCreateView
里面的name赏半,來(lái)反射創(chuàng)建View,這邊用到了一個(gè)技巧:onCreateView
中的name淆两,對(duì)于系統(tǒng)的View断箫,是沒(méi)有'.'符號(hào)的,比如"TextView"我們拿到的直接是TextView琼腔,
但是自定義的View瑰枫,我們拿到的是帶有包名的全部名稱,因此反射時(shí)丹莲,對(duì)于系統(tǒng)的View光坝,我們需要加上系統(tǒng)的包名,自定義的View甥材,則直接使用name盯另。
也不用疑問(wèn)為什么用反射,這樣不是慢嗎洲赵?
因?yàn)橄到y(tǒng)的LayoutInflater
在createView的時(shí)候也是這么做的鸳惯,這邊的代碼都是參考系統(tǒng)的實(shí)現(xiàn)的。
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;
// 系統(tǒng)控件叠萍,沒(méi)有".",因此去創(chuàng)建系統(tǒng)View
if (-1 == name.indexOf('.')) {
// 根據(jù)名稱反射創(chuàng)建
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也會(huì)走
} else {
// 直接根據(jù)名稱創(chuàng)建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的兩參數(shù)構(gòu)造方法創(chuàng)建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是否需要換膚
與創(chuàng)建View一樣苛谷,根據(jù)拿到的onCreateView
里面的AttributeSet attrs
拿到后辅鲸,我們解析attrs
/**
* 拿到attrName和value
* 拿到的value是R.id
*/
String attrName = attrs.getAttributeName(i);//屬性名
String attrValue = attrs.getAttributeValue(i);//屬性值
根據(jù)屬性名和屬性值進(jìn)行判斷,有背景的屬性腹殿,是否符合需要換膚的屬性独悴、
插件化資源注入
我們的皮膚包其實(shí)是APK,是我們寫的另一個(gè)app锣尉,與正式App不同的是刻炒,其只有資源文件,且資源文件需要和主app同名自沧。
1.通過(guò) PackageManager拿皮膚包名
2.拿到皮膚包里面的Resource
但是因?yàn)槲覀兿?code>new Resources()時(shí)候坟奥,發(fā)現(xiàn)其第一個(gè)參數(shù)是AssetManager
,但是AssetManager
的構(gòu)造方法在源碼中被@hide
了,我們沒(méi)有方法拿到這個(gè)類拇厢,但是幸好其類還是能拿到的爱谁,我們直接反射獲取。
我們拿資源的代碼如下旺嬉。
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
/**
* AssetManager assetManager = new AssetManager();
* 這個(gè)方法被@ hide了。厨埋。我們只能通過(guò)反射newInstance
*/
AssetManager assetManager = AssetManager.class.newInstance();
/**
* addAssetPath同樣被系統(tǒng)給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());
/**
* 講皮膚路徑保存邪媳,并設(shè)置不是默認(rèn)皮膚
*/
SkinConfig.saveSkinPath(context, params[0]);
skinPath = skinPkgPath;
isDefaultSkin = false;
/**
* 到此,我們拿到了外置皮膚包的資源
*/
return skinResource;
如何動(dòng)態(tài)的從皮膚包中獲取資源
我們以從皮膚包里面獲取color來(lái)舉例
業(yè)務(wù)端是通過(guò)資源的id來(lái)獲取color的,資源的id也就是一個(gè)在編譯時(shí)就生成的int型雨效。 而皮膚包的也是編譯時(shí)生成的迅涮,因此兩個(gè)id是不一樣的,我們只能通過(guò)資源的id先拿到在我們應(yīng)用里的該id的名字徽龟,再通過(guò)名字去資源包里面拿資源叮姑。
public int getColor(int resId) {
int originColor = ContextCompat.getColor(context, resId);
/**
* 如果皮膚資源包不存在,直接加載
*/
if (mResources == null || isDefaultSkin) {
return originColor;
}
/**
* 每個(gè)皮膚包里面的id是不一樣的据悔,只能通過(guò)名字來(lái)拿传透,id值是不一樣的。
* 1. 獲取默認(rèn)資源的名稱
* 2. 根據(jù)名稱從全局mResources里面獲取值
* 3. 若獲取到了极颓,則獲取顏色返回朱盐,若獲取不到,老老實(shí)實(shí)使用原來(lái)的
*/
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;
}
實(shí)際使用
上面都是我們插件化加載的需要了解的知識(shí)菠隆,真的進(jìn)行框架使用的時(shí)候兵琳,使用了自定義屬性,根據(jù)自定義屬性判斷是否需要換膚骇径。
使用觀察者模式躯肌,所有需要換膚的view都會(huì)存放在Activity一個(gè)集合中,在皮膚管理器通知皮膚更新時(shí)破衔,主動(dòng)更新視圖狀態(tài)清女。
說(shuō)了這么多了,框架的分裝和使用具體可以看我的工程里面的代碼运敢。
https://github.com/Jerey-Jobs/KeepGank
效果如圖:
細(xì)心的朋友會(huì)注意到校仑,每個(gè)主題主頁(yè)的左上角圖片是會(huì)變的,沒(méi)錯(cuò)传惠,那個(gè)圖片是動(dòng)態(tài)加載的資源包里面的迄沫。
代碼見(jiàn):https://github.com/Jerey-Jobs/KeepGank
歡迎star
APK下載 App下載鏈接
本文作者:Anderson/Jerey_Jobs
博客地址 : http://jerey.cn/
簡(jiǎn)書地址 : Anderson大碼渣
github地址 : https://github.com/Jerey-Jobs