Android夜間模式實踐

前言

因爲項目須要,近段時間開發的夜間模式功能。主流的方案以下:android

一、經過切換theme實現
二、經過resource id映射實現
三、經過Android Support Library的實現

方案選擇

  • 切換theme實現夜間模式
    採用這種實現方式的表明是簡書和知乎~實現策略以下:緩存

    1)在xml中定義兩套theme,差異僅僅是顏色不一樣
<!--白天主題-->
    <style name="DayTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="clockBackground">@android:color/white</item>
        <item name="clockTextColor">@android:color/black</item>
    </style>

    <!--夜間主題-->
    <style name="NightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/color3F3F3F</item>
        <item name="colorPrimaryDark">@color/color3A3A3A</item>
        <item name="colorAccent">@color/color868686</item>
        <item name="clockBackground">@color/color3F3F3F</item>
        <item name="clockTextColor">@color/color8A9599</item>
    </style>

自定義顏色:app

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="clockBackground" format="color" />
    <attr name="clockTextColor" format="color" />
</resources>

在layout佈局文件中,如 TextView 裏的 android:textColor="?attr/clockTextColor" 是讓其字體顏色跟隨所設置的 Theme。
2)Java代碼相關實現:ide

@Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
      initData();
      initTheme();
      setContentView(R.layout.activity_day_night);
      initView();
  }

在每次setContentView以前必須調用initTheme方法,由於當 View 建立成功後 ,再去 setTheme 是沒法對 View 的 UI 效果產生影響的。函數

/**
     * 刷新UI界面
     */
    private void refreshUI() {
        TypedValue background = new TypedValue();//背景色
        TypedValue textColor = new TypedValue();//字體顏色
        Resources.Theme theme = getTheme();
        theme.resolveAttribute(R.attr.clockBackground, background, true);
        theme.resolveAttribute(R.attr.clockTextColor, textColor, true);

        mHeaderLayout.setBackgroundResource(background.resourceId);
        for (RelativeLayout layout : mLayoutList) {
            layout.setBackgroundResource(background.resourceId);
        }
        for (CheckBox checkBox : mCheckBoxList) {
            checkBox.setBackgroundResource(background.resourceId);
        }
        for (TextView textView : mTextViewList) {
            textView.setBackgroundResource(background.resourceId);
        }

        Resources resources = getResources();
        for (TextView textView : mTextViewList) {
            textView.setTextColor(resources.getColor(textColor.resourceId));
        }

        int childCount = mRecyclerView.getChildCount();
        for (int childIndex = 0; childIndex < childCount; childIndex++) {
            ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex);
            childView.setBackgroundResource(background.resourceId);
            View infoLayout = childView.findViewById(R.id.info_layout);
            infoLayout.setBackgroundResource(background.resourceId);
            TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname);
            nickName.setBackgroundResource(background.resourceId);
            nickName.setTextColor(resources.getColor(textColor.resourceId));
            TextView motto = (TextView) childView.findViewById(R.id.tv_motto);
            motto.setBackgroundResource(background.resourceId);
            motto.setTextColor(resources.getColor(textColor.resourceId));
        }

        //讓 RecyclerView 緩存在 Pool 中的 Item 失效
        //那麼,若是是ListView,要怎麼作呢?這裏的思路是經過反射拿到 AbsListView 類中的 RecycleBin 對象,而後一樣再用反射去調用 clear 方法
        Class<RecyclerView> recyclerViewClass = RecyclerView.class;
        try {
            Field declaredField = recyclerViewClass.getDeclaredField("mRecycler");
            declaredField.setAccessible(true);
            Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class<?>[]) new Class[0]);
            declaredMethod.setAccessible(true);
            declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]);
            RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool();
            recycledViewPool.clear();

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

        refreshStatusBar();
    }

    /**
     * 刷新 StatusBar
     */
    private void refreshStatusBar() {
        if (Build.VERSION.SDK_INT >= 21) {
            TypedValue typedValue = new TypedValue();
            Resources.Theme theme = getTheme();
            theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
            getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId));
        }
    }

refreshUI函數起到模式切換的做用。經過 TypedValue 和 Theme.resolveAttribute 在代碼中獲取 Theme 中設置的顏色,來從新設置控件的背景色或者字體顏色等等。refreshStatusBar刷新狀態欄。佈局

/**
     * 展現一個切換動畫
     */
    private void showAnimation() {
        final View decorView = getWindow().getDecorView();
        Bitmap cacheBitmap = getCacheBitmapFromView(decorView);
        if (decorView instanceof ViewGroup && cacheBitmap != null) {
            final View view = new View(this);
            view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap));
            ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT);
            ((ViewGroup) decorView).addView(view, layoutParam);
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
            objectAnimator.setDuration(300);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    ((ViewGroup) decorView).removeView(view);
                }
            });
            objectAnimator.start();
        }
    }

    /**
     * 獲取一個 View 的緩存視圖
     *
     * @param view
     * @return
     */
    private Bitmap getCacheBitmapFromView(View view) {
        final boolean drawingCacheEnabled = true;
        view.setDrawingCacheEnabled(drawingCacheEnabled);
        view.buildDrawingCache(drawingCacheEnabled);
        final Bitmap drawingCache = view.getDrawingCache();
        Bitmap bitmap;
        if (drawingCache != null) {
            bitmap = Bitmap.createBitmap(drawingCache);
            view.setDrawingCacheEnabled(false);
        } else {
            bitmap = null;
        }
        return bitmap;
    }

showAnimation 是用於展現一個漸隱效果的屬性動畫,這個屬性做用在哪一個對象上呢?是一個 View ,一個在代碼中動態填充到 DecorView 中的 View。
知乎之因此在夜間模式切換過程當中會有漸隱效果,是由於在切換前進行了截屏,同時將截屏拿到的 Bitmap 設置到動態填充到 DecorView 中的 View 上,並對這個 View 執行一個漸隱的屬性動畫,因此使得咱們可以看到一個漂亮的漸隱過渡的動畫效果。並且在動畫結束的時候再把這個動態添加的 View 給 remove 了,避免了 Bitmap 形成內存飆升問題。測試

  • resource id映射實現夜間模式
    經過id獲取資源時,先將其轉換爲夜間模式對應id,再經過Resources來獲取對應的資源。字體

public static Drawable getDrawable(Context context, int id) {
    return context.getResources().getDrawable(getResId(id));
}
public static int getResId(int defaultResId) {    if (!isNightMode()) {
        return defaultResId;
    }
    if (sResourceMap == null) {
        buildResourceMap();
    }
    int themedResId = sResourceMap.get(defaultResId);
    return themedResId == 0 ? defaultResId : themedResId;
}
private static void buildResourceMap() {
    sResourceMap = new SparseIntArray();
    sResourceMap.put(R.drawable.common_background, R.drawable.common_background_night);
    // ...
}

這個方案簡單粗暴,麻煩的地方和第一種方案同樣:每次添加資源都須要創建映射關係,刷新UI的方式也與第一種方案相似,貌似今日頭條,網易新聞客戶端等主流新聞閱讀應用都是經過這種方式實現的夜間模式。動畫

  • 經過Android Support Library實現
    1)在res目錄中爲夜間模式配置專門的目錄,以-night爲後綴ui

res目錄

2)在Application中設置夜間模式

Application全局設置夜間模式

3)夜間模式切換

夜間模式切換

夜間模式實現

三種方案比較,第二種太暴力,不適合項目後期開發;第一種方法須要作配置的地方比第三種方法多。整體來講,第三種方法最簡單,相似整個app內有一個夜間模式的總開關,切換了之後就不用管了。最後採用第三種方案!
經過Android Support Library實現夜間模式雖然簡單,可是當中也碰到了一些坑。現作一下記錄:
一、 橫屏切換的時候,夜間模式混亂

基於Android Support Library的夜間模式,至關因而support庫來幫忙關鍵相關的資源,有時候會出現錯誤的狀況。好比說app橫豎屏切換以後!!經測試發現,每次調起一個橫屏的Activity,而後退出,整個app的夜間模式就亂了,部分的UI調用的是日間模式的資源~~~

這裏認爲的加了一個多餘的設定:

/**
     * 刷新UI_MODE模式
     */
    public void refreshResources(Activity activity) {

        if (Prop.isNightMode.getBoolean()) {
            updateConfig(activity, Configuration.UI_MODE_NIGHT_YES);
        } else {
            updateConfig(activity, Configuration.UI_MODE_NIGHT_NO);
        }
    }

    /** * google官方bug,暫時解決方案 * 手機切屏後從新設置UI_MODE
     模式(由於在DayNight主題下,切換橫屏後UI_MODE會出錯,會致使
     資源獲取出錯,須要從新設置回來)
     */
    private void updateConfig(Activity activity, int uiNightMode) {
        Configuration newConfig = new
                Configuration(activity.getResources().getConfiguration());
        newConfig.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
        newConfig.uiMode |= uiNightMode;
        activity.getResources().updateConfiguration(newConfig, null);
    }

在每次退出橫屏的時候,調用這個方法,強制刷新一次config
二、 drawable xml文件中部分顏色值 日間/夜間 弄反了
Android Support Library實現的夜間模式,資源的獲取碰到了一些坑。咱們常常會在drawable文件夾中定義一些xml來作背景形狀、背景顏色。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="false" android:state_enabled="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/app_textbook_bg_color"/>
            <corners android:radius="5dp" />
        </shape>
    </item>

</selector>

按照Android Support Library的介紹,須要爲夜間模式建立一個爲night借個人目錄,那麼這裏就能夠有兩種理解:
1)在values/color.xml 和values-night/color.xml分別爲app_textbook_bg_color定義不一樣的色值
2)在values/color.xml 和values-night/color.xml分別爲app_textbook_bg_color定義不一樣的色值;此外,須要分別定義drawable/bg_textbook.xml和drawable-night/bg_textbook.xml,兩個文件的內容能夠同樣。

這裏碰到了一些坑。原先採用的是第一種方法,這樣代碼改動少,看起來一目瞭然。可是,,不一樣廠商的手機會有不同的表現!!部分手機,在夜間模式的時候仍是用的日間的資源;殺了app重進纔會好。
個人理解是Android會對資源作緩存~ 緩存的時候會將app_textbook_color解析出來並緩存;假設日間模式app_textbook_color爲#FFFFFF,咱們設置夜間模式切換,這時候不一樣手機廠商的策略不同,有些廠商會把緩存清除,因此切成夜間模式的時候app_textbook_color的色值會改變,夜間模式正常;可是有些廠商應該不會清理緩存,夜間模式切換以後,拿的是日間模式緩存下的色值,也就是#FFFFFF,這樣就出問題了~~
以上爲我的看法,建議碰到這種狀況,多在drawable下寫一個xml防止個別手機出錯。
三、切換夜間模式須要restartActivity,會閃一下
這也是一個比較坑的地方。夜間模式切換之後,須要從新獲取一遍資源,最簡單的方法是restart一下。如今我採用的就是這種簡單粗暴的方法,用戶體驗比較不友好,後期須要參考知乎的實現,改進實現。

參考連接

Android夜間模式最佳實踐
知乎和簡書的夜間模式實現套路
AppCompat v23.2 - 夜間模式,你所不知道的坑

相關文章
相關標籤/搜索