前言
之前我們總結過B站的皮膚框架MagicaSakura
,也點出了其不足,文章連結:來自B站的開源的MagicaSakura原始碼解析,該框架只能完成普通的換色需求,沒有QQ,網易雲音樂類似的皮膚包的功能。
那麼今天我們就帶來,擁有皮膚載入功能的外掛化換膚框架。框架的分裝和使用具體可以看我的工程裡面的程式碼。
github.com/Jerey-Jobs/…
這樣做有兩個好處:
- 皮膚可以不整合在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
,我們可以看一下鴻神的文章www.tuicool.com/articles/EV…
建立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一個集合中,在皮膚管理器通知皮膚更新時,主動更新檢視狀態。
說了這麼多了,框架的分裝和使用具體可以看我的工程裡面的程式碼。
github.com/Jerey-Jobs/…
效果如圖:
程式碼見:github.com/Jerey-Jobs/…
歡迎star
APK下載 App下載連結
本文作者:Anderson/Jerey_Jobs
部落格地址 : jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : github.com/Jerey-Jobs