插件化實現Android多主題功能原理剖析

前言

以前咱們總結過B站的皮膚框架MagicaSakura,也點出了其不足,文章連接:來自B站的開源的MagicaSakura源碼解析,該框架只能完成普通的換色需求,沒有QQ,網易雲音樂相似的皮膚包的功能。java

那麼今天咱們就帶來,擁有皮膚加載功能的插件化換膚框架。框架的分裝和使用具體能夠看個人工程裏面的代碼。
github.com/Jerey-Jobs/…android

這樣作有兩個好處:
git

  1. 皮膚能夠不集成在apk中,減少apk體積
  2. 動態化增長皮膚,靈活性大,自由度很大

如何實現換膚功能

想固然的,在View建立的時候這是讓咱們應用可以完美的加載皮膚的最好方案。github

那麼咱們知道,對於Activity來講,有一個能夠複寫的方法叫onCreateViewweb

@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…框架

建立View

根據拿到的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是否須要換膚

與建立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/…

效果如圖:

代碼見:github.com/Jerey-Jobs/…

歡迎star

APK下載 App下載連接


本文做者:Anderson/Jerey_Jobs

博客地址 : jerey.cn/

簡書地址 : Anderson大碼渣

github地址 : github.com/Jerey-Jobs

相關文章
相關標籤/搜索