Android夜間模式實踐

前言

由于項目需要,近段時間開發的夜間模式功能。主流的方案如下:
1、通過切換theme實現
2、通過resource id映射實現
3、通過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>

自定義顏色:

<?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代碼相關實現:

@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為后綴
res目錄

2)在Application中設置夜間模式

Application全局設置夜間模式

3)夜間模式切換

夜間模式切換

夜間模式實現

三種方案比較,第二種太暴力,不適合項目后期開發;第一種方法需要做配置的地方比第三種方法多。總體來說,第三種方法最簡單,類似整個app內有一個夜間模式的總開關,切換了以后就不用管了。最后采用第三種方案!
通過Android Support Library實現夜間模式雖然簡單,但是當中也碰到了一些坑。現做一下記錄:
1、 橫屏切換的時候,夜間模式混亂
基于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
2、 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防止個別手機出錯。
3、切換夜間模式需要restartActivity,會閃一下
這也是一個比較坑的地方。夜間模式切換以后,需要重新獲取一遍資源,最簡單的方法是restart一下。現在我采用的就是這種簡單粗暴的方法,用戶體驗比較不友好,后期需要參考知乎的實現,改進實現。

參考鏈接

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容