Android高級開發進階之路3——動態換膚(監聽xml佈局初始化,動態加載類,加載插件資源文件)

動態換膚(監聽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等等屬性,就認爲該控件需要換膚,這個是根據項目的需求而定,沒有強制要求的。下面三個步驟分別對應三段關鍵代碼:

  1. 監聽xml文件實例化View過程
  2. 可以取得每個View名字,通過每個view的名字,就可以動態實例化(實例化不會造成雙重實例化,如果設置了監聽,並且返回view的話,就不會掉用到系統的實例化)
  3. 實例化之後就可以得到相應的屬性,通過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),)

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章