Android動態(tài)換膚實現(xiàn)原理解析

換膚分為動態(tài)換膚和靜態(tài)換膚

靜態(tài)換膚

這種換膚的方式,也就是我們所說的內(nèi)置換膚,就是在APP內(nèi)部放置多套相同的資源站蝠。進行資源的切換。
這種換膚的方式有很多缺點卢佣,比如, 靈活性差区赵,只能更換內(nèi)置的資源惭缰、apk體積太大,在我們的應(yīng)用Apk中等一般圖片文件能占到apk大小的一半左右。
當(dāng)然了,這種方式也并不是一無是處, 比如我們的應(yīng)用內(nèi)笼才,只是普通的 日夜間模式 的切換漱受,并不需要圖片等的更換,只是更換顏色,那這樣的方式就很實用骡送。

動態(tài)換膚

適用于大量皮膚昂羡,用戶選擇下載,像QQ摔踱、網(wǎng)易云音樂這種虐先。它是將皮膚包下載到本地,皮膚包其實是個APK派敷。

換膚包括替換圖片資源蛹批、布局顏色、字體篮愉、文字顏色腐芍、狀態(tài)欄和導(dǎo)航欄顏色。

動態(tài)換膚步驟包括:

  • 采集需要換膚的控件
  • 加載皮膚包
  • 替換資源

采集換膚控件

android解析xml創(chuàng)建view的步驟:

  • setContentView -> window.setContentView()(實現(xiàn)類是PhoneWindow)->mLayoutInflater.inflate() -> inflate .. ->createViewFromTag().

所以我們復(fù)寫了Factory的onCreateView之后试躏,就可以不通過系統(tǒng)層而是自己截獲從xml映射的View進行相關(guān)View創(chuàng)建的操作猪勇,包括對View的屬性進行設(shè)置(比如背景色,字體大小颠蕴,顏色等)以實現(xiàn)換膚的效果泣刹。如果onCreateView返回null的話,會將創(chuàng)建View的操作交給Activity默認(rèn)實現(xiàn)的Factory的onCreateView處理犀被。

1.使用ActivityLifecycleCallbacks椅您,盡可能少的去侵入代碼,在onActivityCreated中監(jiān)聽每個activity的創(chuàng)建寡键。

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
       LayoutInflater layoutInflater = LayoutInflater.from(activity);
       try {
           //系統(tǒng)默認(rèn) LayoutInflater只能設(shè)置一次factory掀泳,所以利用反射解除限制
           Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
           mFactorySet.setAccessible(true);
           mFactorySet.setBoolean(layoutInflater, false);
       } catch (Exception e) {
           e.printStackTrace();
       }

       //添加自定義創(chuàng)建View 工廠
       SkinLayoutFactory factory = new SkinLayoutFactory(activity, skinTypeface);
       layoutInflater.setFactory2(factory);
}

2.在 SkinLayoutFactory中將每個創(chuàng)建的view進行篩選采集

  //根據(jù)tag反射獲取view
    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // 反射 classLoader
        View view = createViewFromTag(name, context, attrs);
        // 自定義View
        if(null ==  view){
            view = createView(name, context, attrs);
        }

        //篩選符合屬性View
        skinAttribute.load(view, attrs);

        return view;
    }

3.將view封裝成對象

    //view的參數(shù)對象
    static class SkinPain {
        String attributeName;
        int resId;

        public SkinPain(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }

    //view對象
     static class SkinView {
        View view;
        List<SkinPain> skinPains;

        public SkinView(View view, List<SkinPain> skinPains) {
            this.view = view;
            this.skinPains = skinPains;
        }
     }

將屬性符合的view保存起來

public class SkinAttribute {
    private static final List<String> mAttributes = new ArrayList<>();

    static {
        mAttributes.add("background");
        mAttributes.add("src");

        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");

        mAttributes.add("skinTypeface");
    }

    private List<SkinView> skinViews = new ArrayList<>();

    public void load(View view, AttributeSet attrs) {
        List<SkinPain> skinPains = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //獲取屬性名字
            String attributeName = attrs.getAttributeName(i);
            if (mAttributes.contains(attributeName)) {
                //獲取屬性對應(yīng)的值
                String attributeValue = attrs.getAttributeValue(i);
                if (attributeValue.startsWith("#")) {
                    continue;
                }
                int resId;
                //判斷前綴字符串 是否是"?"
                //attributeValue  = "?2130903043"
                if (attributeValue.startsWith("?")) {  //系統(tǒng)屬性值
                    //字符串的子字符串  從下標(biāo) 1 位置開始
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    //@1234564
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                if (resId != 0) {
                    SkinPain skinPain = new SkinPain(attributeName, resId);
                    skinPains.add(skinPain);
                }
            }
        }
        //SkinViewSupport是自定義view實現(xiàn)的接口,用來區(qū)分是否需要換膚
        if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
            SkinView skinView = new SkinView(view, skinPains);
            skinView.applySkin(mTypeface);
            skinViews.add(skinView);
        }
    }

    ...

    }

加載皮膚包

加載皮膚包需要我們動態(tài)獲取網(wǎng)絡(luò)下載的皮膚包資源昌腰,問題是我們?nèi)绾渭虞d皮膚包中的資源

Android訪問資源使用的是Resources這個類,但是程序里面通過getContext獲取到的Resources實例實際上是對應(yīng)程序本來的資源的實例膀跌,也就是說這個實例只能加載app里面的資源遭商,想要加載皮膚包里面的就不行了

自己構(gòu)造一個Resources(這個Resources指向的資源就是我們的皮膚包)
看看Resources的構(gòu)造方法,可以看到主要是需要一個AssetManager

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

構(gòu)造一個指向皮膚包的AssetManager捅伤,但是這個AssetManager是不能直接new出來的劫流,這里就使用反射來實例化了

AssetManager assetManager = AssetManager.class.newInstance();

AssetManager有一個addAssetPath方法可以指定資源的位置,可惜這個也只能用反射來調(diào)用

Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, filePath);

再來看看Resources的其他兩個參數(shù),一個是DisplayMetrics祠汇,一個是Configuration仍秤,這兩的就可以直接使用app原來的Resources里面的就可以。

具體代碼如下:

    public void loadSkin(String path) {
        if(TextUtils.isEmpty(path)){
            // 記錄使用默認(rèn)皮膚
            SkinPreference.getInstance().setSkin("");
            //清空資源管理器可很, 皮膚資源屬性等
            SkinResources.getInstance().reset();
        } else {
            try {
                //反射創(chuàng)建AssetManager
                AssetManager manager = AssetManager.class.newInstance();
                // 資料路徑設(shè)置
                Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
                addAssetPath.invoke(manager, path);

                Resources appResources = this.application.getResources();
                Resources skinResources = new Resources(manager,
                        appResources.getDisplayMetrics(), appResources.getConfiguration());

                //記錄當(dāng)前皮膚包
                SkinPreference.getInstance().setSkin(path);
                //獲取外部Apk(皮膚笔Α) 包名
                PackageManager packageManager = this.application.getPackageManager();
                PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
                String packageName = packageArchiveInfo.packageName;

                SkinResources.getInstance().applySkin(skinResources,packageName);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        setChanged();
        //通知觀者者,進行替換資源
        notifyObservers();
    }

替換資源

換膚的核心操作就是替換資源我抠,這里采用觀察者模式苇本,被觀察者是我們的換膚管理類SkinManager,觀察者是我們之前緩存的每個頁面的LayoutInflater.Factory2

    @Override
    public void update(Observable o, Object arg) {
        //狀態(tài)欄
        SkinThemeUtils.updataStatusBarColor(activity);
        //字體
        Typeface skinTypeface = SkinThemeUtils.getSkinTypeface(activity);
        skinAttribute.setTypeface(skinTypeface);
        //更換皮膚
        skinAttribute.applySkin();
    }

applySkin()在去遍歷每個factory緩存的需要換膚的view菜拓,調(diào)用他們的換膚方法

    public void applySkin() {
        for (SkinView mSkinView : skinViews) {
            mSkinView.applySkin(mTypeface);
        }
    }

applySkin方法如下:

        public void applySkin(Typeface typeface) {
            //換字體
            if(view instanceof TextView){
                ((TextView) view).setTypeface(typeface);
            }
            //自定義view換膚
            if(view instanceof SkinViewSupport){
                ((SkinViewSupport)view).applySkin();
            }

            for (SkinPain skinPair : skinPains) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        Object background = SkinResources.getInstance().getBackground(
                                skinPair.resId);
                        //Color
                        if (background instanceof Integer) {
                            view.setBackgroundColor((Integer) background);
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);
                        }
                        break;
                    case "src":
                        background = SkinResources.getInstance().getBackground(skinPair
                                .resId);
                        if (background instanceof Integer) {
                            ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                                    background));
                        } else {
                            ((ImageView) view).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
                                (skinPair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "skinTypeface" :
                        applyTypeface(SkinResources.getInstance().getTypeface(skinPair.resId));
                        break;
                    default:
                        break;
                }
                if (null != left || null != right || null != top || null != bottom) {
                    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
                            bottom);
                }
            }
        }

這里能看到換膚的實現(xiàn)方式就是根據(jù)原始資源Id來獲取皮膚包的資源Id瓣窄,從而加載資源。因此我們要保證app和皮膚包的資源名稱一致

    public Drawable getDrawable(int resId) {
        //如果有皮膚  isDefaultSkin false 沒有就是true
        if (isDefaultSkin) {
            return mAppResources.getDrawable(resId);
        }
        int skinId = getIdentifier(resId);//查找對應(yīng)的資源id
        if (skinId == 0) {
            return mAppResources.getDrawable(resId);
        }
        return mSkinResources.getDrawable(skinId);
    }


    //獲取皮膚包中對應(yīng)資源的id
    public int getIdentifier(int resId) {
        if (isDefaultSkin) {
            return resId;
        }
        //在皮膚包中的資源id不一定就是 當(dāng)前程序的 id
        //獲取對應(yīng)id 在當(dāng)前的名稱 例如colorPrimary
        String resName = mAppResources.getResourceEntryName(resId);//ic_launcher   /colorPrimaryDark
        String resType = mAppResources.getResourceTypeName(resId);//drawable
        int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);//使用皮膚包的Resource
        return skinId;
    }

皮膚包的生成

其實很簡單纳鼎,就是我們重新建立一個項目(這個項目里面的資源名字和需要換膚的項目的資源名字是對應(yīng)的就可以)俺夕,記住我們是通過名字去獲取資源,不是id

  1. 新建工程project
  2. 將換膚的資源文件添加到res文件下贱鄙,無java文件
  3. 直接運行build.gradle劝贸,生成apk文件(注意,運行時Run/Redebug configurations 中Launch Options選擇launch nothing)贰逾,否則build 會報 no default Activty的錯誤悬荣。
  4. 將apk文件重命名,如black.apk重命名為black.skin防止用戶點擊安裝
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市疙剑,隨后出現(xiàn)的幾起案子氯迂,更是在濱河造成了極大的恐慌,老刑警劉巖言缤,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚼蚀,死亡現(xiàn)場離奇詭異,居然都是意外死亡管挟,警方通過查閱死者的電腦和手機轿曙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來僻孝,“玉大人导帝,你說我怎么就攤上這事〈┟” “怎么了您单?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長荞雏。 經(jīng)常有香客問我虐秦,道長平酿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任悦陋,我火速辦了婚禮蜈彼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘俺驶。我一直安慰自己幸逆,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布痒钝。 她就那樣靜靜地躺著秉颗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪送矩。 梳的紋絲不亂的頭發(fā)上蚕甥,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音栋荸,去河邊找鬼菇怀。 笑死,一個胖子當(dāng)著我的面吹牛晌块,可吹牛的內(nèi)容都是我干的爱沟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼匆背,長吁一口氣:“原來是場噩夢啊……” “哼呼伸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钝尸,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤括享,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后珍促,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铃辖,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年猪叙,在試婚紗的時候發(fā)現(xiàn)自己被綠了娇斩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡穴翩,死狀恐怖犬第,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芒帕,我是刑警寧澤歉嗓,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站副签,受9級特大地震影響遥椿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淆储,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一冠场、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧本砰,春花似錦碴裙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至还棱,卻和暖如春载慈,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背珍手。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工办铡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人琳要。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓寡具,卻偏偏與公主長得像,于是被迫代替她去往敵國和親稚补。 傳聞我的和親對象是個殘疾皇子童叠,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

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