android 實現【夜晚模式】的另外一種思路

159345684 8年前發布 | 6K 次閱讀 安卓開發 Android開發 移動開發

預覽

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

在幾個月前更新的 Android Support Library 23.2 中,讓我們認識到了 DayNight Theme 。一看源碼,原來以前在 API 8 的時候就已經有了 night 相關的資源可以設置,只是之前一直不知道怎么使用,后來發現原來還是利用了 AssetManager 相關的API —— Android在指定條件下加載指定文件夾中的資源。 這正是我想要的! 這樣我們只用指定好引用的資源,(比如 @color/colorPrimary ) 那么我就可以在白天加載 values/color.xml 中的資源,晚上加載 values-night/color.xml 中的資源。

v7 已經幫我們完成了這里的功能,放置夜晚資源的問題也已經解決了,可是每次切換 DayNight 模式的時候,需要重啟下 Activity ,這件事情很讓人討厭,原因就是因為重啟后,我們的 Context 就會重新創建, View 也會重新創建,根據當前系統(應用)配置的不同,加載不同的資源。 那我們有沒有可能做到不重啟 Activity 來實現夜間模式呢?其實實現方案很簡單:我們只用記錄好系統渲染xml的時候,當時給 View 的資源id,在特定時刻,重新加載這些資源,然后設置給View即可。接下去我們碰到兩個問題:

  1. 在引入這個庫的情況下,讓開發者少改已有的xml文件,把所有的布局都換為我們指定的布局。
  2. API要盡量簡單,清楚,明白。

上面兩個條件說起來很容易,其實想實現并不是很容易的,還好 AppCompat 給了我一些思路。

來自AppCompat的啟發

當我們引入 appcompat-v7 ,有了 AppCompatActivity 的時候,我們發現我們渲染的 TextView / Button 等組件分別變成了 AppCompatTextView 和 AppCompatButton , 這些組件都是包含在 v7 包中的,很早以前覺得很神奇,當看了 AppCompatActivity 和 AppCompatDelegate 的源碼,知道了 LayoutInflator.Factory 這些東西的工作原理之后,這一切也就不神奇了 —— 它只是在 inflate 的過程中,注入了自己的代碼進去,比如把 TextView 解析成 AppCompatTextView 類,達到對解析結果攔截的目的。

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

像這樣,有興趣的同學可以看看 AppCompatDelegateImplV7 這個類的 installViewFactory 方法的實現。

接下去我們的目的是把 TextView 、 Button 等類換成我們自己的實現—— SkinnableTextView 和 SkinnableButton 。

可以翻到 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;
}
//省略代碼

好了,如果有需要,我們在庫中把所有的類都替換成自己的實現,就能達到目的了,使得那些使用原始控件的開發者,不修改一絲一毫的代碼,渲染出我們定制的控件。

應用DayNightMode

上一節我們解決了自定義 View 替換原始 View 的問題,那么接下去怎么辦呢?這里我們同樣也參考 AppCompat 關于 BackgroundTint 的一些設計方式。首先我們可以看到 AppComatTextView 的聲明:

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

實現了一個 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 {
    //...
}

這樣等于給我的類打了一個標記,外部調用的時候,就可以判斷這個View是否實現了我們的接口,如果實現了接口,就可以調用相關的函數。

我們在 Activity 的基類中,可以如此調用

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));
        }
    }
}

利用遞歸的方式,把所有實現 Skinnable 接口的 View 全部應用了 applyDayNight 方法。 因此開發者使用的時候,只用把 Activity 的繼承改為 SkinnableActivity ,然后在恰當的時機調用 setDayNightMode 即可。

Skinnable在View中具體實現

這節講的是如何解決我們的痛點 —— 不重啟 Activity 應用 DayNight mode 。

那我們的 View 實現 Skinnable 接口中的方法,到底是如何工作的呢,以 SkinnableTextView 為例子。

一般我們對 TextView 應用的樣式有 background 和 textColor ,額外的情況下帶一個 backgroundTint 都是OK的。

首先我們的大前提是,這些資源在 xml 中是用引用的方式傳進來的,什么意思呢,看下面的表格

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

總結起來一句話,就是不應該是絕對值,如果是絕對值的話,我們去改它的值也不符合邏輯。

那么如果是資源引用的方式的話,我們使用 TypedArray 這個對象,是可以獲取到我們引用的資源的id的,也就是 R.color.primaryColor 的具體數值。 我們把這個值保存下來,然后在恰當的時候,利用這個值再去變化后的 Context 中獲取一遍指定的顏色

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

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

我們利用 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);
        }
    }
}

最后,在切換夜間模式的時候,我們調用了 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);
    }
    //省略代碼
}

總結以及缺陷

經過以上幾點的開發,我們使用日/夜模式切換就變得非常容易了,比如我們如果只處理顏色的修改的話,只用在 values/colors.xml 和 values-night/colors.xml 配置好指定顏色在不同模式下的表現形式,再調用 setDayNightMode 方法,就可以完成一鍵切換,不需要在 xml 中添加任何復雜凌亂的東西。

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

同時因為它依賴于 AppCompat DayNight Mode ,它只能作用于日/夜間模式的切換,要想實現 換膚 功能,是做不到的。

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

 

 

來自:http://www.androidchina.net/5118.html

 

 本文由用戶 159345684 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!