(Android-skin-support)換膚框架原理研究并運(yùn)用

背景

很多app需要進(jìn)行換膚,到網(wǎng)絡(luò)上找到了一個(gè)庫--android-skin-support巷燥。很快就實(shí)現(xiàn)了需求赡盘,根據(jù)文檔集成很方便,代碼侵入性也低缰揪。

本來也沒有去深究它的實(shí)現(xiàn)原理陨享,前幾天有個(gè)迭代的需求如下:
美股市場是“綠漲紅跌”,而A股市場是“紅漲綠跌”,產(chǎn)品要求用戶可以自由選擇抛姑。這個(gè)需求跟以前的換膚很類似赞厕,所以就仔細(xì)研究學(xué)習(xí)了"android-skin-support"庫的實(shí)現(xiàn)原理,并根據(jù)其原理實(shí)現(xiàn)“紅漲綠跌”.

原理

  • 使用觀察者模式
    框架抽象了一個(gè)SkinCompatSupportable接口:
    public interface SkinCompatSupportable {
        void applySkin();
    }

所有需要換膚的控件都實(shí)現(xiàn)該接口定硝,在用戶執(zhí)行“換膚”操作時(shí)候皿桑,通知所有實(shí)現(xiàn)該接口的訂閱者執(zhí)行換膚操作applySkin()

按照上面的原理蔬啡,那所有的控件都需要實(shí)現(xiàn)接口SkinCompatSupportable诲侮,那原生的控件怎么辦呢?框架層面把所有常用的原生控件都重寫了一遍星爪,在包skin.support.widget下浆西。框架層在解析布局文件的時(shí)候會把原生的控件替換成實(shí)現(xiàn)了接口SkinCompatSupportable的對應(yīng)控件顽腾〗悖框架層是怎么做的呢?請繼續(xù)查看下文抄肖。

  • 注冊activity生命周期監(jiān)聽器
    查看類skin.support.app.SkinActivityLifecycle,

    private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
        installLayoutFactory(application);
        SkinCompatManager.getInstance()
           .addObserver(getObserver(application));
    }
    
    private void installLayoutFactory(Context context) {
        LayoutInflater layoutInflater = LayoutInflater.from(context);
        try {
            Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
            field.setAccessible(true);
            field.setBoolean(layoutInflater, false);
            LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
        } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (isContextSkinEnable(activity)) {
            installLayoutFactory(activity);
            updateStatusBarColor(activity);
            updateWindowBackground(activity);
            if (activity instanceof SkinCompatSupportable) {
                ((SkinCompatSupportable) activity).applySkin();
            }
    
            if (activity instanceof SkinCompatChangeGreenRed) {
                boolean isSupport = ((SkinCompatChangeGreenRed)activity).isSupportChange();
                ((SkinCompatChangeGreenRed)activity).applyChangeColor(isSupport ? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
            }
        }
    }
    

注意  installLayoutFactory 方法久信,在每個(gè)activity的#onActivityCreated中把自己的LayoutInflaterFactory類(SkinCompatDelegate)設(shè)置進(jìn)去,而把原生控件替換成庫中的控件就在這個(gè)類中實(shí)現(xiàn)的漓摩,并且把所有實(shí)現(xiàn)SkinCompatSupportable接口的觀察者都收集起來裙士。這樣代碼的侵入性就變得很低,使得幾行代碼就可以實(shí)現(xiàn)換膚操作管毙。

或許有人疑惑為什么這樣做就可以實(shí)現(xiàn)xml解析攔截腿椎? 我們再來看看 ```AppCompatActivity``` 的代碼實(shí)現(xiàn)。

```java

 protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && mThemeId != 0) {
        // If DayNight has been applied, we need to re-apply the theme for
        // the changes to take effect. On API 23+, we should bypass
        // setTheme(), which will no-op if the theme ID is identical to the
        // current theme ID.
        if (Build.VERSION.SDK_INT >= 23) {
            onApplyThemeResource(getTheme(), mThemeId, false);
        } else {
            setTheme(mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
    }

我們看到有一個(gè)AppCompatDelegate,它是Activity的委托,AppCompatActivity將大部分生命周期都委托給了AppCompatDelegate,這點(diǎn)可從上面的源碼中可以看出.
繼續(xù)看源碼我們發(fā)現(xiàn)夭咬,解析xml布局的解析起也是在AppCompatDelegate對象中設(shè)置的啃炸。

AppCompatDelegateImplV9.java

@Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

LayoutInflaterCompat.setFactory2(layoutInflater, this);最終是調(diào)用的LayoutInflater的setFactory2()方法,看看實(shí)現(xiàn)

/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
    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);
    }
}

這里有個(gè)小細(xì)節(jié),Factory2只能被設(shè)置一次,設(shè)置完成后mFactorySet屬性就為true,下一次設(shè)置時(shí)被直接拋異常.
那么Factory2有什么用呢?看看其實(shí)現(xiàn)
它是一個(gè)接口,只有一個(gè)方法,看起來是用來創(chuàng)建View的.

綜上所述,我們就可以根據(jù)其機(jī)制實(shí)現(xiàn)xml解析并攔截卓舵,讓解析xml原生控件的時(shí)候返回我們想要的支持“換膚”動作的對應(yīng)控件南用。

  • 攔截xml控件解析事件

代碼如下

SkinCompatViewInflater.java

private View createViewFromFV(Context context, String name, AttributeSet attrs) {
        View view = null;
        if (name.contains(".")) {
            return null;
        }
        switch (name) {
            case "View":
                view = new SkinCompatView(context, attrs);
                break;
            case "LinearLayout":
                view = new SkinCompatLinearLayout(context, attrs);
                break;
            case "RelativeLayout":
                view = new SkinCompatRelativeLayout(context, attrs);
                break;
            case "FrameLayout":
                view = new SkinCompatFrameLayout(context, attrs);
                break;
            case "TextView":
                view = new SkinCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new SkinCompatImageView(context, attrs);
                break;
            case "Button":
                view = new SkinCompatButton(context, attrs);
                break;
            case "EditText":
                view = new SkinCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new SkinCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new SkinCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new SkinCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new SkinCompatRadioButton(context, attrs);
                break;
            case "RadioGroup":
                view = new SkinCompatRadioGroup(context, attrs);
                break;
            case "CheckedTextView":
                view = new SkinCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new SkinCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new SkinCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new SkinCompatSeekBar(context, attrs);
                break;
            case "ProgressBar":
                view = new SkinCompatProgressBar(context, attrs);
                break;
            case "ScrollView":
                view = new SkinCompatScrollView(context, attrs);
                break;
        }
        return view;
    }

這里還是有點(diǎn)迷惑, 那么我們再來看看android創(chuàng)建view的過程

平時(shí)我們最常使用的Activity中的setContentView()設(shè)置布局ID,看看Activity中的實(shí)現(xiàn),

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

調(diào)用的是Window中的setContentView(),而Window只有一個(gè)實(shí)現(xiàn)類,就是PhoneWindow.看看setContentView()實(shí)現(xiàn)

@Override
    public void setContentView(int layoutResID) {
        ...
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }

看到了今天的主角mLayoutInflater,mLayoutInflater是在PhoneWindow的構(gòu)造方法中初始化的.用mLayoutInflater去加載這個(gè)布局(layoutResID).點(diǎn)進(jìn)去看看實(shí)現(xiàn),來看看createViewFromTag()的實(shí)現(xiàn)

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        ...
        try {
            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;
            }

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            ...
            return view;
    }

可以看到如果mFactory2不為空的話,那么就會調(diào)用mFactory2去創(chuàng)建View(mFactory2.onCreateView(parent, name, context, attrs)) . 這句結(jié)論很重要.前面的答案已揭曉.如果設(shè)置了mFactory2就會用mFactory2去創(chuàng)建View.而mFactory2在上面已經(jīng)被我們替換了掏湾。****

  • 加載皮膚

我們前面已經(jīng)了解了換膚的原理裹虫,現(xiàn)在就根據(jù)上述原理進(jìn)行換膚。 該框架提供了幾種加載皮膚的策略融击,前后綴價(jià)值筑公,apk加載等等。

SkinCompatManager.getInstance().loadSkin

總結(jié)

簡單總結(jié)一下原理(本文精髓)

監(jiān)聽APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
在每個(gè)Activity的onCreate()方法調(diào)用時(shí)setFactory(),設(shè)置創(chuàng)建View的工廠.將創(chuàng)建View的瑣事交給SkinCompatViewInflater去處理.
庫中自己重寫了系統(tǒng)的控件(比如View對應(yīng)于庫中的SkinCompatView),實(shí)現(xiàn)換膚接口(接口里面只有一個(gè)applySkin()方法),表示該控件是支持換膚的.并且將這些控件在創(chuàng)建之后收集起來,方便隨時(shí)換膚.
在庫中自己寫的控件里面去解析出一些特殊的屬性(比如:background, textColor),并將其保存起來
在切換皮膚的時(shí)候,遍歷一次之前緩存的View,調(diào)用其實(shí)現(xiàn)的接口方法applySkin(),在applySkin()中從皮膚資源(可以是從網(wǎng)絡(luò)或者本地獲取皮膚包)中獲取資源.獲取資源后設(shè)置其控件的background或textColor等,就可實(shí)現(xiàn)換膚.

借鑒應(yīng)用

現(xiàn)在根據(jù)上述原理低侵入性實(shí)現(xiàn)“紅漲綠跌”尊浪,

  • 接口抽象

定義一個(gè)接口匣屡,讓支持“紅漲綠跌”切換的控件都實(shí)現(xiàn)該接口
SkinCompatChangeGreenRed

  • 所有觀察者都集合起來
  • 執(zhí)行“紅漲綠跌”操作的時(shí)候通知所有觀察者涩拙。
public void notifyChangeColor(boolean isSupport){
        SkinObserver[] arrLocal;

        synchronized (this) {
            arrLocal = observers.toArray(new SkinObserver[observers.size()]);
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            arrLocal[i].updateChangeColor(this, isSupport? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
    }
  • 切換顏色的動作 每個(gè)控制自己處理。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末耸采,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子工育,更是在濱河造成了極大的恐慌虾宇,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件如绸,死亡現(xiàn)場離奇詭異嘱朽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)怔接,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門搪泳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扼脐,你說我怎么就攤上這事岸军。” “怎么了瓦侮?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵艰赞,是天一觀的道長。 經(jīng)常有香客問我肚吏,道長方妖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任罚攀,我火速辦了婚禮党觅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斋泄。我一直安慰自己杯瞻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布是己。 她就那樣靜靜地躺著又兵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卒废。 梳的紋絲不亂的頭發(fā)上沛厨,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音摔认,去河邊找鬼逆皮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛参袱,可吹牛的內(nèi)容都是我干的电谣。 我是一名探鬼主播秽梅,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼剿牺!你這毒婦竟也來了企垦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤晒来,失蹤者是張志新(化名)和其女友劉穎钞诡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湃崩,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡荧降,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攒读。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朵诫。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖薄扁,靈堂內(nèi)的尸體忽然破棺而出剪返,到底是詐尸還是另有隱情,我是刑警寧澤泌辫,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站震放,受9級特大地震影響宾毒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜殿遂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一诈铛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧墨礁,春花似錦幢竹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至驶乾,卻和暖如春邑飒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背级乐。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工疙咸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人风科。 一個(gè)月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓撒轮,卻偏偏與公主長得像乞旦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子题山,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

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