Android動(dòng)態(tài)換膚

現(xiàn)在的很多應(yīng)用都有換膚的功能伞梯,例如QQ玫氢。這類應(yīng)用都是在線下載皮膚包,然后在不重啟的情況下直接完成換膚

示例

demonstrate.gif

原理

  1. Activity setContentView內(nèi)部調(diào)用

關(guān)于setContentView的所有方法,這里調(diào)用了getWindow()返回了Window谜诫,這個(gè)Window在activity的attach方法中被賦值為PhoneWindow
Activity.java源碼:


    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }
    
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }
    
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
      ...
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
      ...
    }

  1. PhoneWindow setContentView內(nèi)部調(diào)用

可以看到實(shí)際調(diào)用了LayoutInflater.inflate方法
PhoneWindow.java源碼:

 @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }
  1. LayoutInflater.inflate內(nèi)部調(diào)用

由源碼可知漾峡,view由Factory2和Factory創(chuàng)建,如果我們hook了Factory2那不是視圖的創(chuàng)建可以由我們說了算
LayoutInflater.java源碼:

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
    
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ...
            final View temp = createViewFromTag(root, name, inflaterContext, attrs);
            result = temp;
            return result;
            ...
        }
    }
    
     View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
            View view;
            if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
        ...
            return view;
    }
    
    public void setFactory2(Factory2 factory) {
    //由此處可知設(shè)置Factory2只能設(shè)置一次喻旷,所以我們設(shè)置時(shí)需要將mFactorySet改成false
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }
  1. Factory2

LayoutInflater.java源碼:

public interface Factory2 extends Factory {
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }

可以看到參數(shù)里面有AttributeSet生逸,我們可以通過AttributeSet篩選需要做處理的屬性,記錄view和對(duì)應(yīng)的屬性且预,然后在換膚時(shí)替換屬性對(duì)應(yīng)的資源槽袄,就可以達(dá)到換膚的目的了,
具體處理邏輯較為復(fù)雜锋谐,可以通過后面提供的源碼查看

SkinPeeler庫

庫代碼傳送門

SkinPeeler庫是基于上面的原理完成的換皮庫遍尺,使用方法:

  1. 導(dǎo)入庫
//root build.gradle
allprojects {
    repositories {
        ...
        maven { url 'https://www.jitpack.io' }
    }
}

//app build.gradle
dependencies {
    implementation 'com.github.ray-tianfeng:skin-peeler:v1.0.0'
}
  1. 使用
  • 換膚 SkinPeeler.getInstance().skin(String skinPath);
    傳入制作好的皮膚包,即可完成換膚

  • 還原 SkinPeeler.getInstance().restore();
    不使用皮膚

  • 換膚監(jiān)聽 SkinPeeler.getInstance().addSkinChangeListener(Activity
    mActivity, SkinPeeler.OnSkinChangeListener mOnSkinChangeListener);

    皮膚切換監(jiān)聽涮拗,完成換皮時(shí)回調(diào)

  • 自定義屬性適配器

    1. 實(shí)現(xiàn)BaseAttrADT.java
    //支持的屬性集合,例如:background乾戏、src、textColor
    public List<String> getAttrName();   
    /**
    * 應(yīng)用皮膚
    * @param targetView 目標(biāo)視圖
    * @param skinResources 皮膚Resources
    * @param skinPackageName 皮膚包包名
    * @param attrName 屬性名稱
    * @param oldValueName 舊值方便通過{@link com.zlong.skinpeeler.utils.IdUtils} 查找皮膚包資源屬性和名稱
    */
    public void applySkin(View targetView, Resources skinResources, String skinPackageName, String attrName, String oldValueName) throws Exception;
    
    /**
    * 恢復(fù)原始皮膚
    * @param targetView 目標(biāo)視圖
    * @param resources 原始 Resources
    * @param attrName 屬性名稱
    * @param oldValueName 舊值
    */
    public void restore(View targetView, Resources resources, String attrName, String oldValueName);
    
    1. 添加屬適配器至管理器SkinPeeler.getInstance().addAttrADT(BaseAttrADT attrADT)
    2. 常用工具類
      IdUtils:資源Id查找工具類三热,通過IdUtils.findResById(int id),查找原包中id對(duì)應(yīng)的類型鼓择、名稱
  • 自定義屬性注意事項(xiàng)

    1. ID
      原包中R.xx.xx對(duì)應(yīng)的資源id不可在皮膚包中使用,必須使用皮膚包中對(duì)應(yīng)資源的id就漾,因?yàn)樵械馁Y源對(duì)應(yīng)的id呐能,和皮膚包中同一資源對(duì)應(yīng)的id不同
    2. 資源查找
      applySkin提供了皮膚包的Resources,那我們可以通過皮膚包資源id獲取對(duì)應(yīng)的資源抑堡,
      我們把原包中的資源id通過IdUtils.findResById查找資源對(duì)應(yīng)的名稱和類型摆出,然后通過Resources.getIdentifier(String name, String defType, String defPackage)查找資源在皮膚包中對(duì)應(yīng)的id朗徊,最后獲取資源就行了

通過第二步我們可以得到資源的id,但是我們不能直接把皮膚包的資源id直接設(shè)置到view上懊蒸,因?yàn)樵つw對(duì)應(yīng)的Resources荣倾,肯定沒有皮膚包對(duì)應(yīng)的資源id。
在代碼中也不能直接設(shè)置資源id骑丸,因?yàn)閾Q膚后,直接設(shè)置資源id妒貌,系統(tǒng)直接通過原始Resources查找的資源通危。需要通過上面的資源查找,直接查找對(duì)應(yīng)的資源灌曙,設(shè)置到對(duì)應(yīng)的view上
庫內(nèi)置了AutoAttrADT.java可以對(duì)照著來實(shí)現(xiàn)自定義屬性

  • 實(shí)現(xiàn)屬性
    庫已經(jīng)通過AutoAttrADT.java實(shí)現(xiàn)了常用屬性的適配
    background菊碟、src、textColor在刺、drawableLeft逆害、drawableTop、drawableRight蚣驼、drawableBottom

  • 皮膚包制作

    1. 創(chuàng)建Module
    2. 將apply plugin: 'com.android.library'修改為apply plugin:
      'com.android.application'魄幕,因?yàn)檫@樣可以生成對(duì)應(yīng)的資源id
    3. 將原項(xiàng)目中res目錄下的所有資源復(fù)制到皮膚包中,layout可以在完成制作后刪除
    4. 替換換膚時(shí)需要修改的資源
    5. 通過build->build bundles->build apk將皮膚包打包
    6. 在對(duì)應(yīng)module的build/outputs/debug
      下有一個(gè)打包好的皮膚包apk颖杏,可以將后綴修改skin纯陨,或者直接使用。修改后綴為了防止用戶安裝和刪除留储。
  • 庫使用注意事項(xiàng)

    • 需要文件讀取權(quán)限翼抠,如果在6.0及以上,需要做權(quán)限處理
    • 包名只能是androidManifest中的packageName获讳,不能在gradle使用applicationId阴颖,因?yàn)镮dUtils通過包名查找R類的。
    • 所有的資源盡量先定義后使用(R.string.xx, R.color.xx, R.drawable.xx)
    • 沉浸式菜單欄適配丐膝,先定義菜單欄顏色量愧,然后在String中定義圖標(biāo)顯示模式,設(shè)置監(jiān)聽尤误。皮膚變化時(shí)侠畔,在回調(diào)中更改狀態(tài)欄顏色及圖標(biāo)顏色

擴(kuò)展1

在上面的實(shí)現(xiàn)過程中有使用到AttributeSet,這個(gè)就是當(dāng)前view的屬性集合损晤,我們是不是可以自定義一個(gè)屬性(圓角背景)软棺。然后在onCreateView解析到此屬性時(shí),
通過java代碼創(chuàng)建一個(gè)drawable尤勋,設(shè)置給view喘落,注意此處自定義的屬性只能在xml中使用茵宪,因?yàn)閂iew不包含這個(gè)自定義的屬性的。!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瘦棋,一起剝皮案震驚了整個(gè)濱河市稀火,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赌朋,老刑警劉巖凰狞,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沛慢,居然都是意外死亡赡若,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門团甲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逾冬,“玉大人,你說我怎么就攤上這事躺苦∩砟澹” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵匹厘,是天一觀的道長嘀趟。 經(jīng)常有香客問我,道長集乔,這世上最難降的妖魔是什么去件? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮扰路,結(jié)果婚禮上尤溜,老公的妹妹穿的比我還像新娘。我一直安慰自己汗唱,他們只是感情好宫莱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哩罪,像睡著了一般授霸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上际插,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天碘耳,我揣著相機(jī)與錄音,去河邊找鬼框弛。 笑死辛辨,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播斗搞,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼指攒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了僻焚?” 一聲冷哼從身側(cè)響起允悦,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虑啤,沒想到半個(gè)月后隙弛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡狞山,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年驶鹉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铣墨。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖办绝,靈堂內(nèi)的尸體忽然破棺而出伊约,到底是詐尸還是另有隱情,我是刑警寧澤孕蝉,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布屡律,位于F島的核電站,受9級(jí)特大地震影響降淮,放射性物質(zhì)發(fā)生泄漏超埋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一佳鳖、第九天 我趴在偏房一處隱蔽的房頂上張望霍殴。 院中可真熱鬧,春花似錦系吩、人聲如沸来庭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽月弛。三九已至,卻和暖如春科盛,著一層夾襖步出監(jiān)牢的瞬間帽衙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工贞绵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厉萝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像冀泻,于是被迫代替她去往敵國和親常侣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354