插件化實(shí)現(xiàn)Android多主題功能原理剖析

前言

之前我們總結(jié)過(guò)B站的皮膚框架MagicaSakura乳幸,也點(diǎn)出了其不足怕品,文章鏈接:來(lái)自B站的開源的MagicaSakura源碼解析治笨,該框架只能完成普通的換色需求考婴,沒(méi)有QQ砌们,網(wǎng)易云音樂(lè)類似的皮膚包的功能杆麸。

那么今天我們就來(lái)看一款擁有皮膚加載功能的插件化換膚框架。其已經(jīng)集成在我的應(yīng)用https://github.com/Jerey-Jobs/KeepGank中浪感。

這樣做有兩個(gè)好處:

  1. 皮膚可以不集成在apk中昔头,減小apk體積
  2. 動(dòng)態(tài)化增加皮膚,靈活性大影兽,自由度很大

如何實(shí)現(xiàn)換膚功能

想當(dāng)然的揭斧,在View創(chuàng)建的時(shí)候這是讓我們應(yīng)用能夠完美的加載皮膚的最好方案。

那么我們知道峻堰,對(duì)于Activity來(lái)說(shuō)讹开,有一個(gè)可以復(fù)寫的方法叫onCreateView

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return super.onCreateView(parent, name, context, attrs);
}

我們的view的創(chuàng)建就是通過(guò)這個(gè)方法來(lái)的,我們甚至可以通過(guò)復(fù)寫這個(gè)方法捐名,實(shí)現(xiàn)view的替換旦万,比如本來(lái)要的是TextView,我們直接給它替換成Button.而這個(gè)方法其實(shí)是實(shí)現(xiàn)的LayoutInflaterFactory接口镶蹋。

關(guān)于LayoutInflaterFactory成艘,我們可以看一下鴻神的文章http://www.tuicool.com/articles/EVzEny6

創(chuàng)建View

根據(jù)拿到的onCreateView里面的name赏半,來(lái)反射創(chuàng)建View,這邊用到了一個(gè)技巧:onCreateView中的name淆两,對(duì)于系統(tǒng)的View断箫,是沒(méi)有'.'符號(hào)的,比如"TextView"我們拿到的直接是TextView琼腔,
但是自定義的View瑰枫,我們拿到的是帶有包名的全部名稱,因此反射時(shí)丹莲,對(duì)于系統(tǒng)的View光坝,我們需要加上系統(tǒng)的包名,自定義的View甥材,則直接使用name盯另。

也不用疑問(wèn)為什么用反射,這樣不是慢嗎洲赵?

因?yàn)橄到y(tǒng)的LayoutInflater在createView的時(shí)候也是這么做的鸳惯,這邊的代碼都是參考系統(tǒng)的實(shí)現(xiàn)的。

    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;
            // 系統(tǒng)控件叠萍,沒(méi)有".",因此去創(chuàng)建系統(tǒng)View
            if (-1 == name.indexOf('.')) {
                // 根據(jù)名稱反射創(chuàng)建
                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也會(huì)走
            } else {
                // 直接根據(jù)名稱創(chuàng)建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的兩參數(shù)構(gòu)造方法創(chuàng)建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是否需要換膚

與創(chuàng)建View一樣苛谷,根據(jù)拿到的onCreateView里面的AttributeSet attrs

拿到后辅鲸,我們解析attrs

/**
 * 拿到attrName和value
 * 拿到的value是R.id
 */
String attrName = attrs.getAttributeName(i);//屬性名
String attrValue = attrs.getAttributeValue(i);//屬性值

根據(jù)屬性名和屬性值進(jìn)行判斷,有背景的屬性腹殿,是否符合需要換膚的屬性独悴、

插件化資源注入

我們的皮膚包其實(shí)是APK,是我們寫的另一個(gè)app锣尉,與正式App不同的是刻炒,其只有資源文件,且資源文件需要和主app同名自沧。

1.通過(guò) PackageManager拿皮膚包名
2.拿到皮膚包里面的Resource

但是因?yàn)槲覀兿?code>new Resources()時(shí)候坟奥,發(fā)現(xiàn)其第一個(gè)參數(shù)是AssetManager,但是AssetManager的構(gòu)造方法在源碼中被@hide了,我們沒(méi)有方法拿到這個(gè)類拇厢,但是幸好其類還是能拿到的爱谁,我們直接反射獲取。

我們拿資源的代碼如下旺嬉。

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
/**
 * AssetManager assetManager = new AssetManager();
 * 這個(gè)方法被@ hide了。厨埋。我們只能通過(guò)反射newInstance
 */
AssetManager assetManager = AssetManager.class.newInstance();
/**
 * addAssetPath同樣被系統(tǒng)給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());
/**
 * 講皮膚路徑保存邪媳,并設(shè)置不是默認(rèn)皮膚
 */
SkinConfig.saveSkinPath(context, params[0]);
skinPath = skinPkgPath;
isDefaultSkin = false;
/**
 * 到此,我們拿到了外置皮膚包的資源
 */
return skinResource;

如何動(dòng)態(tài)的從皮膚包中獲取資源

我們以從皮膚包里面獲取color來(lái)舉例

業(yè)務(wù)端是通過(guò)資源的id來(lái)獲取color的,資源的id也就是一個(gè)在編譯時(shí)就生成的int型雨效。 而皮膚包的也是編譯時(shí)生成的迅涮,因此兩個(gè)id是不一樣的,我們只能通過(guò)資源的id先拿到在我們應(yīng)用里的該id的名字徽龟,再通過(guò)名字去資源包里面拿資源叮姑。

public int getColor(int resId) {
    int originColor = ContextCompat.getColor(context, resId);
    /**
     * 如果皮膚資源包不存在,直接加載
     */
    if (mResources == null || isDefaultSkin) {
        return originColor;
    }
    /**
     * 每個(gè)皮膚包里面的id是不一樣的据悔,只能通過(guò)名字來(lái)拿传透,id值是不一樣的。
     * 1. 獲取默認(rèn)資源的名稱
     * 2. 根據(jù)名稱從全局mResources里面獲取值
     * 3. 若獲取到了极颓,則獲取顏色返回朱盐,若獲取不到,老老實(shí)實(shí)使用原來(lái)的
     */
    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;
}

實(shí)際使用

上面都是我們插件化加載的需要了解的知識(shí)菠隆,真的進(jìn)行框架使用的時(shí)候兵琳,使用了自定義屬性,根據(jù)自定義屬性判斷是否需要換膚骇径。

使用觀察者模式躯肌,所有需要換膚的view都會(huì)存放在Activity一個(gè)集合中,在皮膚管理器通知皮膚更新時(shí)破衔,主動(dòng)更新視圖狀態(tài)清女。

說(shuō)了這么多了,框架的分裝和使用具體可以看我的工程里面的代碼运敢。
https://github.com/Jerey-Jobs/KeepGank

效果如圖:
細(xì)心的朋友會(huì)注意到校仑,每個(gè)主題主頁(yè)的左上角圖片是會(huì)變的,沒(méi)錯(cuò)传惠,那個(gè)圖片是動(dòng)態(tài)加載的資源包里面的迄沫。

代碼見(jiàn):https://github.com/Jerey-Jobs/KeepGank

歡迎star

APK下載 App下載鏈接


本文作者:Anderson/Jerey_Jobs

博客地址 : http://jerey.cn/

簡(jiǎn)書地址 : Anderson大碼渣

github地址 : https://github.com/Jerey-Jobs

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市卦方,隨后出現(xiàn)的幾起案子羊瘩,更是在濱河造成了極大的恐慌,老刑警劉巖盼砍,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尘吗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡浇坐,警方通過(guò)查閱死者的電腦和手機(jī)睬捶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)近刘,“玉大人擒贸,你說(shuō)我怎么就攤上這事臀晃。” “怎么了介劫?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵徽惋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我座韵,道長(zhǎng)险绘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任誉碴,我火速辦了婚禮宦棺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘翔烁。我一直安慰自己渺氧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布蹬屹。 她就那樣靜靜地躺著侣背,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慨默。 梳的紋絲不亂的頭發(fā)上贩耐,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音厦取,去河邊找鬼潮太。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虾攻,可吹牛的內(nèi)容都是我干的铡买。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼霎箍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼奇钞!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起漂坏,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤景埃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后顶别,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谷徙,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年驯绎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了完慧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡剩失,死狀恐怖屈尼,靈堂內(nèi)的尸體忽然破棺而出蛤织,到底是詐尸還是另有隱情,我是刑警寧澤鸿染,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站乞巧,受9級(jí)特大地震影響涨椒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绽媒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一蚕冬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧是辕,春花似錦囤热、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至疙教,卻和暖如春棺聊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贞谓。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工限佩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人裸弦。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓祟同,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親理疙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子晕城,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言: 本文主要講述如何在項(xiàng)目中,在不重啟應(yīng)用的情況下沪斟,實(shí)現(xiàn)動(dòng)態(tài)換膚的效果广辰。換膚這塊做的比較好的,有網(wǎng)易云音樂(lè)主之,q...
    Yagami3zZ閱讀 13,639評(píng)論 5 51
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,167評(píng)論 25 707
  • 今天再給大家?guī)?lái)一篇干貨择吊。 Android的主題換膚 ,可插件化提供皮膚包槽奕,無(wú)需Activity的重啟直接實(shí)現(xiàn)無(wú)縫...
    _SOLID閱讀 99,638評(píng)論 147 1,120
  • 我沒(méi)想過(guò)自己會(huì)在此時(shí)此景下迫切的想要寫些什么來(lái)疏解自己粤攒,有的時(shí)候會(huì)感激現(xiàn)在的自己所森,起碼學(xué)會(huì)了寫些不那么直接的文字囱持,...
    芥子之人閱讀 137評(píng)論 0 0
  • 今天周日。繼昨天的超級(jí)忙碌之后焕济,今天終于可以好好放松一下了纷妆。 今天早上睡到6點(diǎn)半才醒來(lái),看了半小時(shí)視頻晴弃,起床練了將...
    靈動(dòng)的蘭蘭閱讀 251評(píng)論 2 2