動態換膚(監聽xml佈局初始化,動態加載類,加載插件資源文件)
一般小公司是不會接觸到換膚的,尤其是動態下載皮膚插件來實現換膚。那麼,今天我們來一探究竟。如何實現加載從網上下載的皮膚插件,並且替換到相應的控件中!
大致涉及到4個步驟:
1、下載皮膚插件(通常爲apk,後期用skin.apk來表示皮膚插件)到本地
2、根據皮膚插件skin.apk的絕對路徑,加載插件裏的資源文件
3、準確找到需要換膚的控件
4、應用skin.apk裏的資源,修改相應的屬性
第一步下載皮膚插件就是簡單地下載文件操作,這裏就不做詳細介紹,重點部分已經用紅色加粗字體標記出來了!下面介紹一下關鍵的api,最後完整代碼鏈接。
加載插件apk中的資源文件
public void setSkinPath(String skinPath) {
this.skinPath = skinPath;
//獲取包管理類
PackageManager packageManager = context.getPackageManager();
//獲取插件包信息類
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
//獲取插件包名
packageName = packageInfo.packageName;
//獲取插件的資源文件
AssetManager assets = null;
try {
assets = AssetManager.class.newInstance();
Method method = assets.getClass().getMethod("addAssetPath", String.class);//方法名,參數類型
method.invoke(assets, packageName);//調用該方法的對象,傳入的參數
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
resources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
找到需要換膚的控件
如果包含src,textColor,background等等屬性,就認爲該控件需要換膚,這個是根據項目的需求而定,沒有強制要求的。下面三個步驟分別對應三段關鍵代碼:
- 監聽xml文件實例化View過程
- 可以取得每個View名字,通過每個view的名字,就可以動態實例化(實例化不會造成雙重實例化,如果設置了監聽,並且返回view的話,就不會掉用到系統的實例化)
- 實例化之後就可以得到相應的屬性,通過AttributeSet獲取到該View所有的屬性,例如id,backgroundColor,@xxx(例如color)/xxx(例如white),獲取到了信息其實就是對應R.xxx.xxx(例如R.color.white),根據這個名字去找到皮膚資源文件對應名字的資源,如果存在,那就替換,否則就不替換!
//監聽xml文件實例化View過程
skinFactory = new SkinLayoutInflateFactory();
LayoutInflaterCompat.setFactory2(getLayoutInflater(), skinFactory);
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {
//...
//可以取得每個View名字,通過每個view的名字,就可以動態實例化(實例化不會造成雙重實例化,如果設置了監聽,並且返回view的話,就不會掉用到系統的實例化)
/**
* 一個完整的類名,生成一個
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
View view = null;
try {
Class aClass = context.getClassLoader().loadClass(name);
Constructor<? extends View> constructor = aClass.getConstructor(new Class[]{Context.class, AttributeSet.class});
view = constructor.newInstance(context, attrs);
} catch (Exception e) {
e.printStackTrace();
}
return view;
}
}
class SkinLayoutInflateFactory implements LayoutInflater.Factory2 {
//...
//實例化之後就可以得到相應的屬性,通過AttributeSet獲取到該View所有的屬性,例如id,backgroundColor,@xxx(例如color)/xxx(例如white),獲取到了信息其實就是對應R.xxx.xxx(例如R.color.white),根據這個名字去找到皮膚資源文件對應名字的資源,如果存在,那就替換,否則就不替換!
/**
* 如果控件已經實例化,那麼我們就去判斷這個控件是否符合換膚的需求
* @param view
* @param name
* @param attrs
*/
private void parseView(View view, String name, AttributeSet attrs) {
List<SkinItem> listSkinItem = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);
String idStr = attrs.getAttributeValue(i);
if (attrName.contains("background")
||attrName.contains("textColor")
||attrName.contains("src")
||attrName.contains("color")
) {
//獲取屬性值id
int id = Integer.parseInt(idStr.substring(1));
String entryType = view.getResources().getResourceTypeName(id);
String entryName = view.getResources().getResourceEntryName(id);
SkinItem skinItem = new SkinItem(attrName, id, entryName, entryType);
listSkinItem.add(skinItem);
}
SkinView skinView = new SkinView(view, listSkinItem);
listWillBeChangeSkinView.add(skinView);
//skinView.apply();
}
SkinManager.getInstance().applyNewSkin(listWillBeChangeSkinView);
}
//...
}
修改相應的屬性
前面一步拿到皮膚插件中對應名字資源的id,等等信息。通過id,利用皮膚插件的resource去查找這個id,如果有,就設置新的,沒有就設置原來的。
public int getColor(int id) {
if(resources == null) return id;
//根據id獲取屬性值的名字
String entryName = context.getResources().getResourceEntryName(id);
//根據id獲取屬性值的類型
String entryType = context.getResources().getResourceTypeName(id);
//根據屬性值的名稱、屬性值的類型、包名,查看該包名下這個屬性的id
int newId = resources.getIdentifier(entryName, entryType, packageName);
if (newId == 0) {
return id;
}
return resources.getColor(newId);
}
初學者積分用得比較快,爲了幫助大家省點,項目我就放到github給你們吧,喜歡的朋友高擡貴手給我一個star,謝謝!
源碼傳送門:https://github.com/KubyWong/JetpackSample
總結:
下面再來簡單總結一下整體流程,
加載apk外部皮膚插件,獲取的resource操作資源對象(apk路徑->包名->AssetsManager->Resource,類的動態加載newInstance,動態載入方法invoke)
重寫系統實例化xml文件中的view的過程(setFactory2()方法設置一個接口,複寫方法即可)
拿到view實例中的各種屬性,如id,屬性類型type,值的類型valuetype,值的名字valuename(涉及到resource和attrbuteset操作)
替換view中各種屬性的值(涉及到根據id拿到屬性名,屬性類型context.getResources().getResourceTypeName(id)
;根據屬性名,屬性類型,包名,拿到相應的包名下單資源id,關鍵代碼resources.getIdentifier(entryName, entryType, packageName),)