Android應(yīng)用換膚實(shí)現(xiàn)原理解密

App換膚在很多大廠的應(yīng)用中都是比較常見的;比如:網(wǎng)易云音樂抄肖、QQ音樂咒锻、酷狗音樂等應(yīng)用中都是可以見到的府框。這些應(yīng)用是如何做到換膚的效果的,接下來就分析一下是如何實(shí)現(xiàn)的若厚。

1.控件采集

要實(shí)現(xiàn)換膚肯定是要獲取到需要換膚的控件;比如ImageView拦英、TextView等這些控件得提前去把它們收集起來在用戶點(diǎn)擊換膚的時(shí)候就批量的找到對(duì)應(yīng)的資源替換的一個(gè)過程;控件是如何做到集中采集的呢?這個(gè)就需要去看View的加載過程了测秸,這里就不去分析了疤估,在LayoutInflater中的createViewFromTag()中可以找到每一個(gè)View都是通過mFactory2.onCreateView();的方法創(chuàng)建的;那么我們需要得到每一個(gè)View則需要自已去創(chuàng)建這些View灾常,則要去自己實(shí)現(xiàn)這個(gè)OnCreateView方法來替換掉系統(tǒng)用自已實(shí)現(xiàn)的;具體代碼實(shí)現(xiàn)如下:

/**
 * Create by Wayne on 2020/3/7
 * Describe:View的控件采集
 */
public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2 {

    private static final String TAG = SkinLayoutInflaterFactory.class.getName();

    //記錄對(duì)應(yīng)View的構(gòu)造函數(shù)
    private static final Map<String,Constructor<? extends View>> mConstructorMap = new HashMap<>();

    private static final Class<?>[] mConstructorSignature = new Class[]{Context.class,AttributeSet.class};
    private Activity activity;
    private SkinAttribute skinAttribute = null;


    private static final String []  mClassPrefixList = {
            "android.widget.",
            "android.view.",
            "android.webkit."
    };

    public SkinLayoutInflaterFactory(Activity activity, Typeface typeface) {
        this.activity = activity;
        skinAttribute = new SkinAttribute(typeface);
    }

    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attributeSet) {
        View view = createViewFormTag(name,context,attributeSet);
        if(view == null){
            view = createView(name,context,attributeSet);
        }
        if(view != null){
            skinAttribute.load(view,attributeSet,activity);
        }

        return view;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull String s, @NonNull Context context, @NonNull AttributeSet attributeSet) {
        return null;
    }
   private View createViewFormTag(String name,Context context,AttributeSet attrs){
        if(-1 != name.indexOf('.')){
            return null;
        }
        for (int i = 0; i < mClassPrefixList.length; i++) {
            return createView(mClassPrefixList[i]+name,context,attrs);
        }
        return null;
    }

    private View createView(String name,Context context,AttributeSet attrs){
        Constructor<? extends View> constructor = findConstructor(context,name);
        try {
            return constructor.newInstance(context,attrs);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    private Constructor<? extends View> findConstructor(Context context, String name) {
        Constructor<? extends View> constructor = mConstructorMap.get(name);
        if(null == constructor){
            try {
                Class<? extends View> clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
                constructor = clazz.getConstructor(mConstructorSignature);
                mConstructorMap.put(name,constructor);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return constructor;
    }
}

接下來就需要替換掉系統(tǒng)中Factory2的對(duì)象,需要用自已實(shí)現(xiàn)的類以達(dá)到控件采集的目的,在類寫好后可以調(diào)用LayoytInflaterCompat中的setFactory2();方法把我們寫好的類設(shè)置進(jìn)去替換掉系統(tǒng)的铃拇。

    SkinLayoutInflaterFactory skinLayoutInflaterFactory = new SkinLayoutInflaterFactory(activity,typeface);
    LayoutInflaterCompat.setFactory2(layoutInflater,skinLayoutInflaterFactory);

所有的控件已經(jīng)采集到了;則需要去分析出哪一些控件中包含需要換膚的屬性保存起來,比如:background钞瀑、src、textColor等屬性都需要換膚;具體要采集的控件屬性如下:

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("progressDrawable");
        mAttributes.add("thumb");
        mAttributes.add("style");
        mAttributes.add("track");
        mAttributes.add("button");
        mAttributes.add("skinTypeface");
    }

每一個(gè)控件創(chuàng)建完成后都去和這個(gè)集合中的屬性對(duì)比,如果有匹配則把該View和對(duì)應(yīng)的屬性保存起來;代碼實(shí)現(xiàn)如下:

 //用于記錄換膚需要操作的View與屬性信息
    private List<SkinView> mSkinViews = new ArrayList<>();

    public void load(View view, AttributeSet attrs, Activity activity){
        this.mActivity = activity;
        List<SkinPair> mSkinPars = new ArrayList<>();
        for (int i = 0;i<attrs.getAttributeCount();i++) {
            //獲得屬性名
            String attributeName = attrs.getAttributeName(i);
            Log.d(TAG,"屬性名為:"+attributeName);
            if(mAttributes.contains(attributeName)){
                String attributeValue = attrs.getAttributeValue(i);
                //如果是Color的 以#開頭的表示寫死的顏色不需要換膚
                if(attributeValue.startsWith("#")){
                    continue;
                }
                int resId = 0;
                if(attributeValue.startsWith("?")){
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = SkinUtils.getResId(view.getContext(),new int[]{attrId})[0];
                     String typeName = view.getResources().getResourceTypeName(resId);
                     SkinPair skinPair = new SkinPair(attributeName,typeName,mActivity.getComponentName().toShortString(),resId);
                     if(!mSkinPars.contains(skinPair)){
                         mSkinPars.add(skinPair);
                     }
                }
                if(attributeName.startsWith("@") && attributeValue.startsWith("@0")){
                    //正常是以 @ 開頭,但是有可能會(huì)有以@0開頭的,比如沒有寫id的,就會(huì)以@0開頭的,所以得過慮掉這些
                    resId = Integer.parseInt(attributeValue.substring(1));
                    String typeName = view.getResources().getResourceTypeName(resId);
                    SkinPair skinPair = new SkinPair(attributeName,typeName,mActivity.getComponentName().toShortString(),resId);
                    if(!mSkinPars.contains(skinPair)){
                        mSkinPars.add(skinPair);
                    }

                }

            }
        }

        if(!mSkinPars.isEmpty()){
            SkinView skinView = new SkinView(view,mSkinPars,activity.getComponentName().toShortString());
            skinView.applySkin(typeface);
            mSkinViews.add(skinView);
        }else if(view instanceof TextView || view instanceof SkinViewSupport){
            //沒有屬性滿足但是需要修改字體
            SkinView skinView = new SkinView(view,mSkinPars,activity.getComponentName().toShortString());
            skinView.applySkin(typeface);
            mSkinViews.add(skinView);
        }
    }

2.皮膚包加載

上面已經(jīng)把所有的View已經(jīng)采集完了慷荔,那么接下來就需要把皮膚包加載進(jìn)來這樣才能去獲取皮膚包中的資源雕什,Android中的資源管理都是通過AssetManager來管理的,那么則需要去創(chuàng)建AssetManager的實(shí)例显晶,注意:這里不能用App中的AssetManager贷岸,因?yàn)槠つw包是從SD卡加載進(jìn)來的,不是原App中的磷雇,所以不能使用偿警,需要重新創(chuàng)建AssetManager的實(shí)例才可以,通過反射去創(chuàng)建AssetManager的實(shí)例;代碼實(shí)現(xiàn):

 /**
     * 皮膚包加載
     * @param skinPath 皮膚路徑 如果為空則使用默認(rèn)皮膚
     */
    public void loadSkin(String skinPath){
        if(TextUtils.isEmpty(skinPath)){
            SkinResource.getInstance().reset();

            //清空資源管理器 皮膚資源屬性
            SkinResource.getInstance().reset();
        }else {
            try {
                AssetManager assetManager = AssetManager.class.newInstance();
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
                addAssetPath.invoke(addAssetPath,skinPath);
                Resources appResource = mContext.getResources();
                Resources skinResource = new Resources(assetManager,appResource.getDisplayMetrics(),appResource.getConfiguration());
                SkinPreference.getInstance().setSkinPath(skinPath);
                PackageManager mPm = mContext.getPackageManager();
                PackageInfo info = mPm.getPackageArchiveInfo(skinPath,PackageManager.GET_ACTIVITIES);
                mSkinPackageName = info.packageName;
                SkinResource.getInstance().applySkin(skinResource,mSkinPackageName);
            }catch (Exception e){
                onLoadSkinFailure(e);
                e.printStackTrace();
            }
        }
        setChanged();
        notifyObservers(null);
    }

皮膚包加載進(jìn)來后就可以實(shí)例Resource對(duì)象了唯笙,這樣就可以拿到皮膚包的Resource對(duì)象就可以拿到皮膚中的資源文件;當(dāng)換膚時(shí)則需要用皮膚包的Resource去取對(duì)應(yīng)的資源螟蒸。

3.控件的資源替換

上面控件已經(jīng)采集,皮膚包也已經(jīng)加載后內(nèi)存后則就可以把需要換膚的控件進(jìn)行皮膚替換睁本,代碼實(shí)現(xiàn):

public void applySkin(Typeface typeface) {
            applyTypeFace(typeface);
            applySkinSupport();
            for (SkinPair skinPair : skinPairs) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        Object background = SkinResource.getInstance().getBackground(skinPair
                                .resId);
                        if(skinPair.typeName.equals("color")){
                            view.setBackgroundColor(SkinResource.getInstance().getColor(skinPair.resId));
                        }else if(skinPair.typeName.equals("drawable") || skinPair.typeName.equals("mipmap")){
                            if (background instanceof Integer) {
                                view.setBackgroundColor((int) background);
                            } else {
                                view.setBackgroundDrawable((Drawable) background);
                            }
                        }

                        break;
                    case "src":
                        background = SkinResource.getInstance().getBackground(skinPair
                                .resId);
                        if (skinPair.typeName.equals("drawable") || skinPair.typeName.equals("mipmap")) {
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                                if (view instanceof ImageButton) {
                                    ImageButton imageButton = (ImageButton) view;
                                    if (background instanceof Integer) {
                                        imageButton.setImageDrawable(new ColorDrawable((Integer) background));
                                    } else {
                                        imageButton.setImageDrawable((Drawable) background);
                                    }
                                } else if (view instanceof EditText) {
                                    EditText editText = (EditText) view;
                                    if (background instanceof Integer) {
                                        editText.setBackground(new ColorDrawable((Integer) background));
                                    } else {
                                        editText.setBackground((Drawable) background);
                                    }

                                } else if (view instanceof ImageView) {
                                    ImageView imageView = (ImageView) view;

                                    if (background instanceof Integer) {
                                        imageView.setImageDrawable(new ColorDrawable((Integer) background));
                                    } else {
                                        imageView.setImageDrawable((Drawable) background);
                                    }
                                } else {
                                    if (background instanceof Integer) {
                                        view.setBackground(new ColorDrawable((Integer) background));
                                    } else {
                                        view.setBackground((Drawable) background);
                                    }
                                }
                            }
                        }
                        break;
                    case "textColor":
                        if (view instanceof TextView) {
                            ((TextView) view).setTextColor(SkinResource.getInstance().getColorStateList
                                    (skinPair.resId));
                        } else if (view instanceof Button) {
                            ((Button) view).setTextColor(SkinResource.getInstance().getColorStateList
                                    (skinPair.resId));
                        }
                        break;
        }
}

根據(jù)具體的View判斷來進(jìn)行資源替換完成換膚操作;以上就是換膚三個(gè)步驟;換膚分析的是總理思路尿庐,還有一細(xì)節(jié)處理這里就大概描述一下忠怖,如果要完善還要考慮內(nèi)存泄露的問題呢堰,因?yàn)樾枰獡Q膚的View是保存起來的,所以當(dāng)Activity在onDestroyed的時(shí)候則需要把Activity中的View去釋放掉凡泣,這里就涉及Activity的生命周期管理了枉疼,這樣可以去實(shí)現(xiàn) Application中的ActivityLifecycleCallbacks去監(jiān)聽Activity的生命周期;還有就是字體的替換和狀態(tài)欄的換膚也沒有分析,其實(shí)這兩個(gè)很簡單字體的替換也是加載皮膚包中字體出來然后TextView動(dòng)態(tài)設(shè)置字體文件就可以達(dá)到字體替換效果鞋拟,狀態(tài)欄的換膚可以通過Activity拿到Window去替換顏色就可以了骂维。以上就是換膚的全部內(nèi)容,如果發(fā)現(xiàn)哪里描述有問題或者是錯(cuò)誤可以私信指正贺纲,畢竟本人知識(shí)水平有限難免出錯(cuò);需要demo也可以聯(lián)系我航闺,如果文章讓你get到干貨請(qǐng)給我一個(gè)贊吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猴誊,一起剝皮案震驚了整個(gè)濱河市潦刃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌懈叹,老刑警劉巖乖杠,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異澄成,居然都是意外死亡胧洒,警方通過查閱死者的電腦和手機(jī)畏吓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卫漫,“玉大人菲饼,你說我怎么就攤上這事⊙炊担” “怎么了巴粪?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長粥谬。 經(jīng)常有香客問我肛根,道長,這世上最難降的妖魔是什么漏策? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任派哲,我火速辦了婚禮,結(jié)果婚禮上掺喻,老公的妹妹穿的比我還像新娘芭届。我一直安慰自己,他們只是感情好感耙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布褂乍。 她就那樣靜靜地躺著,像睡著了一般即硼。 火紅的嫁衣襯著肌膚如雪逃片。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天只酥,我揣著相機(jī)與錄音褥实,去河邊找鬼。 笑死裂允,一個(gè)胖子當(dāng)著我的面吹牛损离,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绝编,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼僻澎,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼十饥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拳恋,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤谬运,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后伞访,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厚掷,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冒黑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年抡爹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冬竟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片民逼。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖笑诅,靈堂內(nèi)的尸體忽然破棺而出映屋,到底是詐尸還是另有隱情同蜻,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布瘫析,位于F島的核電站默责,受9級(jí)特大地震影響桃序,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜奇适,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一嚷往、第九天 我趴在偏房一處隱蔽的房頂上張望皮仁。 院中可真熱鬧贷祈,春花似錦、人聲如沸宣谈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗦嗡。三九已至,卻和暖如春饭玲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茄厘。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留次哈,地道東北人胎署。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓窑滞,卻偏偏與公主長得像琼牧,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哀卫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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