android 實現(xiàn)【夜晚模式】的另外一種思路

源碼地址

在一切開始之前,我只想用正當(dāng)?shù)姆绞酵┖保蚯蟾魑坏囊粋€star

呵呵
呵呵

https://github.com/geminiwen/skin-sprite

預(yù)覽


預(yù)覽
預(yù)覽

在寫SegmentFault for Android 4.0的過程中缸棵,因為原先采用的夜間模式仿村,代碼著實不好看缰雇,于是我又開始挖坑了。

在幾個月前更新的Android Support Library 23.2中嘹屯,讓我們認識到了DayNight Theme。一看源碼从撼,原來以前在API 8的時候就已經(jīng)有了night相關(guān)的資源可以設(shè)置州弟,只是之前一直不知道怎么使用,后來發(fā)現(xiàn)原來還是利用了AssetManager相關(guān)的API —— Android在指定條件下加載指定文件夾中的資源低零。 這正是我想要的呆馁! 這樣我們只用指定好引用的資源,(比如@color/colorPrimary) 那么我就可以在白天加載values/color.xml中的資源毁兆,晚上加載values-night/color.xml中的資源浙滤。

白天加載values的資源,晚上加載values-night的資源
白天加載values的資源气堕,晚上加載values-night的資源

v7已經(jīng)幫我們完成了這里的功能纺腊,放置夜晚資源的問題也已經(jīng)解決了畔咧,可是每次切換DayNight模式的時候,需要重啟下Activity揖膜,這件事情很讓人討厭誓沸,原因就是因為重啟后,我們的Context就會重新創(chuàng)建壹粟,View也會重新創(chuàng)建拜隧,根據(jù)當(dāng)前系統(tǒng)(應(yīng)用)配置的不同,加載不同的資源趁仙。 那我們有沒有可能做到不重啟Activity來實現(xiàn)夜間模式呢洪添?其實實現(xiàn)方案很簡單:我們只用記錄好系統(tǒng)渲染xml的時候,當(dāng)時給View的資源id雀费,在特定時刻干奢,重新加載這些資源,然后設(shè)置給View即可盏袄。接下去我們碰到兩個問題:

  1. 在引入這個庫的情況下忿峻,讓開發(fā)者少改已有的xml文件,把所有的布局都換為我們指定的布局辕羽。
  2. API要盡量簡單逛尚,清楚,明白刁愿。

上面兩個條件說起來很容易黑低,其實想實現(xiàn)并不是很容易的,還好AppCompat給了我一些思路酌毡。

來自AppCompat的啟發(fā)

當(dāng)我們引入appcompat-v7克握,有了AppCompatActivity的時候,我們發(fā)現(xiàn)我們渲染的TextView/Button等組件分別變成了AppCompatTextViewAppCompatButton枷踏, 這些組件都是包含在v7包中的菩暗,很早以前覺得很神奇,當(dāng)看了AppCompatActivityAppCompatDelegate的源碼旭蠕,知道了LayoutInflator.Factory這些東西的工作原理之后停团,這一切也就不神奇了 —— 它只是在inflate的過程中,注入了自己的代碼進去掏熬,比如把TextView解析成AppCompatTextView類佑稠,達到對解析結(jié)果攔截的目的。

OK旗芬,借助這個方法舌胶,我們可以在Activity.onCreate中,注入我們自己的LayoutInflatorFactory

clipboard.png
clipboard.png

像這樣疮丛,有興趣的同學(xué)可以看看AppCompatDelegateImplV7這個類的installViewFactory方法的實現(xiàn)幔嫂。
接下去我們的目的是把TextView辆它、Button等類換成我們自己的實現(xiàn)——SkinnableTextViewSkinnableButton
可以翻到AppCompatViewInflater這個類的源碼履恩,其實很清晰了:

 public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

這里完成的工作就是把XML中的一些Tag解析為java的類實例锰茉,我們可以依樣畫葫蘆,只不過把其中的AppCompatTextView換成SkinnableTextView

//省略代碼
switch (name) {
   case "TextView":
       view = new SkinnableTextView(context, attrs);
       break;
}
//省略代碼

好了切心,如果有需要飒筑,我們在庫中把所有的類都替換成自己的實現(xiàn),就能達到目的了绽昏,使得那些使用原始控件的開發(fā)者协屡,不修改一絲一毫的代碼,渲染出我們定制的控件而涉。

應(yīng)用DayNightMode

上一節(jié)我們解決了自定義View替換原始View的問題著瓶,那么接下去怎么辦呢联予?這里我們同樣也參考AppCompat關(guān)于BackgroundTint的一些設(shè)計方式啼县。首先我們可以看到AppComatTextView的聲明:

public class AppCompatTextView extends TextView implements TintableBackgroundView {
//...
}

實現(xiàn)了一個TintableBackgroundView的接口,而我們使用ViewCompat.setSupportBackgroundTint的時候沸久,可以找到這么一條:

static void setBackgroundTintList(View view, ColorStateList tintList) {
    if (view instanceof TintableBackgroundView) {
        ((TintableBackgroundView) view).setSupportBackgroundTintList(tintList);
    }
}

利用OO的特性季眷,很輕松的判斷這個View是否支持我們想要的特性,這時候我也聲明了一個接口Skinnable

public class SkinnableTextView extends AppCompatTextView implements Skinnable {
    //...
}

這樣等于給我的類打了一個標記卷胯,外部調(diào)用的時候子刮,就可以判斷這個View是否實現(xiàn)了我們的接口,如果實現(xiàn)了接口窑睁,就可以調(diào)用相關(guān)的函數(shù)挺峡。

我們在Activity的基類中,可以如此調(diào)用

private void applyDayNightForView(View view) {
    if (view instanceof Skinnable) {
        Skinnable skinnable = (Skinnable) view;
        if (skinnable.isSkinnable()) {
            skinnable.applyDayNight();
        }
    }
    if (view instanceof ViewGroup) {
        ViewGroup parent = (ViewGroup)view;
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            applyDayNightForView(parent.getChildAt(i));
        }
    }
}

利用遞歸的方式担钮,把所有實現(xiàn)Skinnable接口的View全部應(yīng)用了applyDayNight方法橱赠。 因此開發(fā)者使用的時候,只用把Activity的繼承改為SkinnableActivity箫津,然后在恰當(dāng)?shù)臅r機調(diào)用setDayNightMode即可狭姨。

Skinnable在View中具體實現(xiàn)

這節(jié)講的是如何解決我們的痛點 —— 不重啟Activity應(yīng)用DayNight mode

android:textColor="@color/primaryColor" android:textColor="#fff"
android:textColor="?attr/colorPrimary" android:textColor="#000"

那我們的View實現(xiàn)Skinnable接口中的方法苏遥,到底是如何工作的呢饼拍,以SkinnableTextView為例子。
一般我們對TextView應(yīng)用的樣式有backgroundtextColor田炭,額外的情況下帶一個backgroundTint都是OK的师抄。
首先我們的大前提是,這些資源在xml中是用引用的方式傳進來的教硫,什么意思呢司澎,看下面的表格

android:textColor="@color/primaryColor" android:textColor="#fff"
android:textColor="?attr/colorPrimary" android:textColor="#000"

總結(jié)起來一句話欺缘,就是不應(yīng)該是絕對值,如果是絕對值的話挤安,我們?nèi)ジ乃闹狄膊环线壿嫛?/p>

那么如果是資源引用的方式的話谚殊,我們使用TypedArray這個對象,是可以獲取到我們引用的資源的id的蛤铜,也就是R.color.primaryColor的具體數(shù)值嫩絮。 我們把這個值保存下來,然后在恰當(dāng)?shù)臅r候围肥,利用這個值再去變化后的Context中獲取一遍指定的顏色

ContextCompat.getColor(context, R.color.primaryColor);

這時候我們獲取到的實際值剿干,context就會根據(jù)系統(tǒng)的配置去正確的文件夾下找我們想要的資源了。

我們利用TypedArray能獲取到資源的id穆刻,使用TypedArray.getResourceId方法即可置尔,傳入屬性的索引值就行。

public void storeAttributeResource(TypedArray a, int[] styleable) {
    int size = a.getIndexCount();
    for (int index = 0; index < size; index ++) {
        int resourceId = a.getResourceId(index, -1);
        int key = styleable[index];
        if (resourceId != -1) {
            mResourceMap.put(key, resourceId);
        }
    }
}

最后氢伟,在切換夜間模式的時候榜轿,我們調(diào)用了applyDayNight方法,具體代碼如下:

@Override
public void applyDayNight() {
    Context context = getContext();
    int key;

    key = R.styleable.SkinnableView[R.styleable.SkinnableView_android_background];
    Integer backgroundResource = mAttrsHelper.getAttributeResource(key);
    if (backgroundResource != null) {
        Drawable background = ContextCompat.getDrawable(context, backgroundResource);
        //這時候獲取到的background是符合上下文的
        setBackgroundDrawable(background);
    }
    //省略代碼
}

總結(jié)以及缺陷

經(jīng)過以上幾點的開發(fā)朵锣,我們使用日/夜模式切換就變得非常容易了谬盐,比如我們?nèi)绻惶幚眍伾男薷牡脑挘挥迷?code>values/colors.xml和values-night/colors.xml配置好指定顏色在不同模式下的表現(xiàn)形式诚些,再調(diào)用setDayNightMode方法飞傀,就可以完成一鍵切換,不需要在xml中添加任何復(fù)雜凌亂的東西诬烹。

因為在配置上節(jié)省了許多代碼砸烦,那我們的約定就變得比較冗長了,如果想進行自定義View的換膚的話绞吁,就需要手動去實現(xiàn)Skinnable接口幢痘,實現(xiàn)applyDayNight方法,開發(fā)者這時候就需要去做一些緩存資源id的操作掀泳。

同時因為它依賴于AppCompat DayNight Mode雪隧,它只能作用于日/夜間模式的切換,要想實現(xiàn)換膚功能员舵,是做不到的脑沿。

這兩點是缺陷,同時也是和市面上其他換膚庫最不同的地方马僻。但是我們把骯臟的代碼隱藏在頂部實現(xiàn)里庄拇,就是為了業(yè)務(wù)邏輯層代碼的干凈和整潔。

希望各位會喜歡,然后有問題可以留言或者在github上給我提PR措近,非常感謝溶弟。

Github Repo 地址:https://github.com/geminiwen/skin-sprite

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瞭郑,隨后出現(xiàn)的幾起案子辜御,更是在濱河造成了極大的恐慌,老刑警劉巖屈张,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件擒权,死亡現(xiàn)場離奇詭異,居然都是意外死亡阁谆,警方通過查閱死者的電腦和手機碳抄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來场绿,“玉大人剖效,你說我怎么就攤上這事⊙娴粒” “怎么了璧尸?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長姨谷。 經(jīng)常有香客問我逗宁,道長映九,這世上最難降的妖魔是什么梦湘? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮件甥,結(jié)果婚禮上捌议,老公的妹妹穿的比我還像新娘。我一直安慰自己引有,他們只是感情好瓣颅,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著譬正,像睡著了一般宫补。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上曾我,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天粉怕,我揣著相機與錄音,去河邊找鬼抒巢。 笑死贫贝,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播稚晚,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼崇堵,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了客燕?” 一聲冷哼從身側(cè)響起鸳劳,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎也搓,沒想到半個月后棍辕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡还绘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年楚昭,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拍顷。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡抚太,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出昔案,到底是詐尸還是另有隱情尿贫,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布踏揣,位于F島的核電站庆亡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏捞稿。R本人自食惡果不足惜又谋,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娱局。 院中可真熱鬧彰亥,春花似錦、人聲如沸衰齐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耻涛。三九已至废酷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抹缕,已是汗流浹背澈蟆。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留歉嗓,地道東北人丰介。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哮幢。 傳聞我的和親對象是個殘疾皇子带膀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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