Android 插件換膚的原理和源碼分析
在學習安卓插件化開發的路上,有一處風景是肯定要觀賞的,那就是基于插件的應用換膚了。
插件換膚原理概述
基于 插件進行應用換膚 的技術大致可以分為兩個方面:
- 如何加載插件包中各式各樣的資源,如 drawable、color 等。
- 如何定位到需要換膚的控件,并優雅地更改樣式,如 無須重啟換膚 等。
針對第一個問題,相關的研究已經比較多了,通過研究 Resource 類 的源碼,在其構造函數中有個 AssetManager 類參數,而最終獲取資源都是通過 AssetManager 來獲取的。
于是,通過構造 AssetManager 并生成插件的 Resource 類,就可以加載插件包中的資源。
針對第二個問題,首先是定位需要換膚的控件,大多數是通過在控件的 XML 布局中添加 標識 ,標識那些需要換膚的控件及需要改變的屬性。然后再通過控件的 set 方法改變屬性即可。
在改變控件的屬性時,若每次都通過遍歷頁面所有 View 來換膚則性能開銷太大,通過 LayoutInflater.Factory 接口在加載布局文件時便先處理所有 View 的屬性,只保存那些需要換膚的控件,則會優化性能。
當然還是有其他問題待解決的,例如: Resource 類加載的資源 ID 沖突, 插件 Resource 不同安卓版本的兼容性,使用 LayoutInflater.Factory 是一種侵入式編程,會干涉系統構造 View 的過程,如何無侵入的換膚,動態加載的控件如何進行換膚 等等問題……
看到很多換膚的框架都參考了該工程,也來分析一下其原理。
再了解插件換膚的大致原理后,再去分析換膚框架的源碼就變得簡單多了,無非就是要解決上述的問題,下面就對 Android-Skin-Loader 源碼進行分析。
動態加載插件資源
在 SkinManager 的 load 方法中,加載了插件包,并且得到了插件的資源 Resource 。
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
// 得到插件包名,根據包名和資源 ID 得到資源
skinPackageName = mInfo.packageName;
// 通過反射構造 AssetManager 類
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
// 反射調用 addAssetPath 方法
addAssetPath.invoke(assetManager, skinPkgPath);
// 得到皮膚插件的 Resource
Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
// 保存皮膚包路徑
SkinConfig.saveSkinPath(context, skinPkgPath);
有了插件資源 Resource ,就可以去得到想要的資源了。
換膚控件及屬性的標識
Android-Skin-Loader 框架自定義了一個 enable 的屬性,用在 XML 文件中來標識哪些控件需要進行換膚。
并且 Android-Skin-Loader 在需要繼承的基類 BaseActivity 、 BaseFragment 、 BaseFragmentActivity 中都設置了 LayoutInflater.Factory ,以便在布局加載之前進行預操作,也就是保存那些 需要換膚的控件 和識別 需要換膚的屬性,這里 換膚控件 和 換膚屬性 兩個東西要區別開,它們所要進行的操作是不一樣的,要先找到 換膚控件 ,然后再去找它的 換膚屬性 。
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// 從 AttributeSet 中得到換膚屬性,判斷是否需要進行換膚
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
if (!isSkinEnable){
return null; // 不需要換膚的,則返回 null,由系統構造
}
// 構造需要換膚的 View
View view = createView(context, name, attrs);
if (view == null){
return null;
}
// 解析需要換膚 View 的屬性
parseSkinAttr(context, attrs, view);
return view;
}
以上代碼就是我們所說的侵入式編程,干擾了系統構造 View 的過程,所做的工作就是找出需要換膚的 View 并交由下一步進行解析。
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
for (int i = 0; i < attrs.getAttributeCount(); i++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(!AttrFactory.isSupportedAttr(attrName)){
continue; // AttrFactory 定義了哪些屬性支持換膚,若該屬性不支持換膚就跳過繼續
}
if(attrValue.startsWith("@")){ // 表明是引用類型,例如 @color/red
int id = Integer.parseInt(attrValue.substring(1));
String typeName = context.getResources().getResourceTypeName(id); // 類型名
String entryName = context.getResources().getResourceEntryName(id); // 入口名
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
}
}
if(!ListUtils.isEmpty(viewAttrs)){
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
// 將需要 換膚的控件 和 換膚的屬性 這兩個東西進行保存
mSkinItems.add(skinItem);
if(SkinManager.getInstance().isExternalSkin()){
skinItem.apply(); // 如果是外部的皮膚,則要 apply 一下,防止換膚不及時
}
}
}
上述代碼的作用就是從需要換膚的控件中,找到那些需要更改的屬性,并將它保存在 mSkinItems 全局變量中。
Android-Skin-Loader 框架中有一個抽象基類 SkinAttr 表示需要更改的屬性,而具體需要更改的屬性都是繼承自 SkinAttr ,所以如果想要更改更多的屬性,就必須自己添加對應的 SkinAttr 類了。
解析完屬性后,換膚控件及屬性都被保存在了 mSkinItems 全局變量中,這樣就完成了加載布局界面的預操作。
顯然,Android-Skin-Loader 框架對于解決找出待更改的屬性這一問題,并不是那么的方便,并且干預了系統構造 View 的過程。
下面研究 hongyang 大神的解決思路: AndroidChangeSkin 代碼。
AndroidChangeSkin 框架并沒有使用 LayoutInflater.Factory 方案了,采用了一種無侵入的方案。
對于標識需要換膚的控件這一問題,AndroidChangeSkin 并沒有再添加自定義屬性,而是使用 View 自帶的 tag 屬性。并在在 tag 屬性的字符串值中,傳遞了要換膚的標識、要換膚的屬性、要換膚的屬性名。通過解析這三者來完成標識的任務。這樣就不必要對每個屬性都進行操作了。
//傳入activity,找到content元素,遞歸遍歷所有的子View,根據tag命名,記錄需要換膚的View
public static List<SkinView> getSkinViews(Activity activity)
{
List<SkinView> skinViews = new ArrayList<SkinView>();
ViewGroup content = (ViewGroup) activity.findViewById(android.R.id.content);
addSkinViews(content, skinViews); // 找到需要換膚的 View 放到 skinViews 里面
return skinViews;
}
/**
* 得到換膚的 View ,如果 tag 為 null 或者 tag 不是字符串,則返回 null
* 解析需要換膚的 View ,得到所有需要更改的屬性 SkinAttr
*/
public static SkinView getSkinView(View view)
{
Object tag = view.getTag(R.id.skin_tag_id);
if (tag == null)
{
tag = view.getTag();
}
if (tag == null) return null;
if (!(tag instanceof String)) return null;
String tagStr = (String) tag;
List<SkinAttr> skinAttrs = parseTag(tagStr);
if (!skinAttrs.isEmpty())
{
changeViewTag(view);
return new SkinView(view, skinAttrs);
}
return null;
}
AndroidChangeSkin 完成查找 換膚控件 和 換膚屬性 兩大任務,之所以說是無侵入性,就是因為它是從 Activity 布局的頂層開始遍歷的,是在布局文件加載完成之后。
換膚操作及響應回調
完成了 View 的標識及查找任務之后,剩下就是最終的換膚操作了。
要做到無須重啟應用和 Activity 完成換膚,Android-Skin-Loader 和 AndroidChangeSkin 都是基于 觀察者模式 來處理的,也就是通過回調方法。
收到進行換膚的指令時,在頁面中響應回調方法,通過皮膚插件的 Resource 加載對應的資源完成替換。
在此之前,我們找到了需要換膚的 View 和需要更改的屬性,那么最終的換膚操作也就是由這些 View 來設置它的新屬性,插件資源的加載也就是發生在這里了。也只有在這個時候,才會去加載皮膚插件中的資源,而之前的第一步只是構造插件的 Resource 并沒有加載資源。
Android-Skin-Loader 是為每一個需要更改的屬性定義了一個類,并在此類中去加載資源。
public class TextColorAttr extends SkinAttr {
@Override
public void apply(View view) {
if(view instanceof TextView){
TextView tv = (TextView)view;
if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
tv.setTextColor(SkinManager.getInstance().convertToColorStateList(attrValueRefId));
而 androidChangeSkin 則沒有編寫那么多類,采用了枚舉類型來更改屬性,同樣也是在屬性中加載資源。
public enum SkinAttrType
{
BACKGROUND("background"){
@Override
public void apply(View view, String resName)
{
Drawable drawable = getResourceManager().getDrawableByName(resName);
if (drawable != null)
{
view.setBackgroundDrawable(drawable);
} else{
try{
int color = getResourceManager().getColor(resName);
view.setBackgroundColor(color);
},
至于,基于觀察者模式來響應換膚操作就比較簡單了,看過代碼很容易知道是怎么一個編程方式了。
動態添加 View 的換膚
以上只是分析了從布局文件中加載的換膚,對于運行時動態添加的 View 同樣可以換膚,只不過不能再 XML 文件中添加屬性了。
畢竟即使是在運行時添加的 View 也是要先確定好需求,編寫對應代碼的。只不過少了標識該 View 是否需要換膚這一步,直接找到需要換膚的屬性就好了,在收到換膚指令時,也是加載插件資源,直接更改屬性即可。
總結
在總結了基于插件換膚的原理和相關代碼之后,發現其實應用換膚也不是那么難嘛….
參考
1、http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/
2、http://blog.csdn.net/zhi184816/article/details/53436761
3、http://www.jianshu.com/p/af7c0585dd5b
來自:http://www.glumes.com/android-change-skin-by-plugin/