以前咱們總結過B站的皮膚框架MagicaSakura
,也點出了其不足,文章連接:來自B站的開源的MagicaSakura源碼解析,該框架只能完成普通的換色需求,沒有QQ,網易雲音樂相似的皮膚包的功能。java
那麼今天咱們就帶來,擁有皮膚加載功能的插件化換膚框架。框架的分裝和使用具體能夠看個人工程裏面的代碼。
github.com/Jerey-Jobs/…android
這樣作有兩個好處:
git
想固然的,在View建立的時候這是讓咱們應用可以完美的加載皮膚的最好方案。github
那麼咱們知道,對於Activity來講,有一個能夠複寫的方法叫onCreateView
web
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return super.onCreateView(parent, name, context, attrs);
}複製代碼
咱們的view的建立就是經過這個方法來的,咱們甚至能夠經過複寫這個方法,實現view的替換,好比原本要的是TextView,咱們直接給它替換成Button.而這個方法實際上是實現的LayoutInflaterFactory
接口。app
關於LayoutInflaterFactory
,咱們能夠看一下鴻神的文章www.tuicool.com/articles/EV…框架
根據拿到的onCreateView
裏面的name,來反射建立View,這邊用到了一個技巧:onCreateView
中的name,對於系統的View,是沒有'.'符號的,好比"TextView"咱們拿到的直接是TextView,
可是自定義的View,咱們拿到的是帶有包名的所有名稱,所以反射時,對於系統的View,咱們須要加上系統的包名,自定義的View,則直接使用name。ide
也不用疑問爲何用反射,這樣不是慢嗎?
由於系統的LayoutInflater
在createView的時候也是這麼作的,這邊的代碼都是參考系統的實現的。ui
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同樣,根據拿到的onCreateView
裏面的AttributeSet attrs
spa
拿到後,咱們解析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/…
效果如圖:
歡迎star
本文做者:Anderson/Jerey_Jobs
博客地址 : jerey.cn/
簡書地址 : Anderson大碼渣
github地址 : github.com/Jerey-Jobs