Android開發筆記(一百六十三)高仿京東的沉浸式狀態欄

前面的文章介紹瞭如何實現廣告輪播的Banner效果,本想可以告一段落。然而某天產品經理心血來潮,拿着蘋果手機,要求像iOS那樣把廣告圖頂到狀態欄這兒。剛接到這需求,不禁倒吸一口冷氣,又要安卓開發去實現iOS的效果,真是強人所難。翻了翻資料,發現修改狀態欄的顏色倒是可行,但要把輪播圖頂上去就不容易了。再瞅瞅淘寶和噹噹,原來兩個大廠的App都沒做出這個效果。正想跟產品經理說這個實現不了,誰料產品大姐笑盈盈地走過來,指着手機說道:“你看,做成京東這樣就行了。”盯着手機看了半晌,京東這廝還真的讓輪播圖插進狀態欄了,於是瞬間石化。下面是京東App的首頁頭部截圖:


每當此時,便是程序員最煎熬的時候,人家都做得,爲啥你做不得?只好繼續尋尋覓覓,又找到另一個電商App,它在Android6.0手機上也完美實現了狀態欄懸浮效果,但是在Android4.4手機運行時仍然沒能覆蓋狀態欄。可見這真不是一個省油的燈,許多人用的App尚且未能解決懸浮狀態欄的兼容性問題。該電商App的首頁截圖如下所示,其中左圖爲Android6.0手機上的運行界面,此時狀態欄浮在輪播圖上面;右圖爲Android4.4手機的運行界面,此時狀態欄依舊與輪播圖涇渭分明。



早期的Android版本姑且不提,Android遲至4.4纔開始支持沉浸式狀態欄,編碼的時候通過Window對象的setAttributes方法來設置窗口屬性的標誌位。其中標誌位WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS用於控制頂部狀態欄是否透明,標誌位WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION用於控制底部導航欄是否透明。具體的實現代碼如下所示:
        // Android4.4的沉浸式狀態欄寫法
        Window window = activity.getWindow();
        WindowManager.LayoutParams attributes = window.getAttributes();
        int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
        // 底部導航欄也可以弄成透明的
        //int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
        attributes.flags |= flagTranslucentStatus;
        //attributes.flags |= flagTranslucentNavigation;
        window.setAttributes(attributes);
到了Android5.0之後版本,系統允許直接定製狀態欄的顏色,例如調用Window對象的setStatusBarColor方法即可設置頂部狀態欄的背景色,調用Window對象的setNavigationBarColor方法即可設置底部導航欄的背景色。不過狀態欄的懸浮開關發生了變化,要想讓狀態欄變透明,最新的方式是調用DecorView對象的setSystemUiVisibility方法來設置標誌位。詳細的標誌位設置代碼如下所示:
        // Android5.0之後的沉浸式狀態欄寫法
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        // 兩個標誌位要結合使用,表示讓應用的主體內容佔用系統狀態欄的空間
        // 第三個標誌位可讓底部導航欄變透明View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
        int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        decorView.setSystemUiVisibility(option);
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
然而以上的處理過程只解決了事情的一個方面,即成功將狀態欄懸浮在主頁面之上,或者說將主頁面沉沒到狀態欄之下。可是事情的另一方面——把懸浮着的狀態欄恢復原狀——並沒有得到解決,甚至給狀態欄換個背景色都不行。譬如說乘船過河,Android時常派了渡船運送乘客,可是當你到達彼岸之後,卻發現回程的船隻不見了蹤影。就恢復狀態欄的原狀而言,設置標誌位是行不通的,幸好過河不一定靠船,還有一招叫做瞞天過海。雖然主頁面已經和狀態欄重疊在了一起,沒法強行把它倆拆散,但我們可以叫主頁面讓一讓,不要跟狀態欄捱得這麼緊,就是給主頁面設置一段頂端空白topMargin,表示主權在我、不妨讓你三尺,於是主頁面讓出一段空白,看起來就與狀態欄井水不犯河水了。如此一來,狀態欄的懸浮和恢復操作便是可逆的了,如果移除主頁面的頂端空白,狀態欄就產生懸浮效果;如果添加主頁面的頂端空白,狀態欄就恢復原狀。
對於Android4.4,情況還會更加特殊,因爲系統沒有提供設置狀態欄顏色的方法,所以只能手工搞個假冒的狀態欄來佔坑。先將這個冒牌狀態欄(其內部沒有別的控件)染上開發者指定的顏色,然後與系統自帶的狀態欄重合,於是乎偷樑換柱彷彿給狀態欄換了一件衣裳。修改之後的狀態欄背景設置代碼如下所示(兼容Android4.4,以及5.0以上版本這兩種情況):
    // 重置狀態欄。即把狀態欄顏色恢復爲系統默認的黑色
    public static void reset(Activity activity) {
        setStatusBarColor(activity, Color.BLACK);
    }

    // 設置狀態欄的背景色。對於Android4.4和Android5.0以上版本要區分處理
    public static void setStatusBarColor(Activity activity, int color) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                activity.getWindow().setStatusBarColor(color);
                // 底部導航欄顏色也可以由系統設置
                //activity.getWindow().setNavigationBarColor(color);
            } else {
                setKitKatStatusBarColor(activity, color);
            }
            if (color == Color.TRANSPARENT) { // 透明背景表示要懸浮狀態欄
                removeMarginTop(activity);
            } else { // 其它背景表示要恢復狀態欄
                addMarginTop(activity);
            }
        }
    }

    private static final String TAG_FAKE_STATUS_BAR_VIEW = "statusBarView";
    private static final String TAG_MARGIN_ADDED = "marginAdded";
    // 添加頂部間隔,留出狀態欄的位置
    private static void addMarginTop(Activity activity) {
        Window window = activity.getWindow();
        ViewGroup contentView = (ViewGroup) window.findViewById(Window.ID_ANDROID_CONTENT);
        View child = contentView.getChildAt(0);
        if (!TAG_MARGIN_ADDED.equals(child.getTag())) {
            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) child.getLayoutParams();
            // 添加的間隔大小就是狀態欄的高度
            params.topMargin += getStatusBarHeight(activity);
            child.setLayoutParams(params);
            child.setTag(TAG_MARGIN_ADDED);
        }
    }

    // 移除頂部間隔,霸佔狀態欄的位置
    private static void removeMarginTop(Activity activity) {
        Window window = activity.getWindow();
        ViewGroup contentView = (ViewGroup) window.findViewById(Window.ID_ANDROID_CONTENT);
        View child = contentView.getChildAt(0);
        if (TAG_MARGIN_ADDED.equals(child.getTag())) {
            FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) child.getLayoutParams();
            // 移除的間隔大小就是狀態欄的高度
            params.topMargin -= getStatusBarHeight(activity);
            child.setLayoutParams(params);
            child.setTag(null);
        }
    }

    // 對於Android4.4,系統沒有提供設置狀態欄顏色的方法,只能手工搞個假冒的狀態欄來佔坑
    private static void setKitKatStatusBarColor(Activity activity, int statusBarColor) {
        Window window = activity.getWindow();
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        // 先移除已有的冒牌狀態欄
        View fakeView = decorView.findViewWithTag(TAG_FAKE_STATUS_BAR_VIEW);
        if (fakeView != null) {
            decorView.removeView(fakeView);
        }
        // 再添加新來的冒牌狀態欄
        View statusBarView = new View(activity);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));
        params.gravity = Gravity.TOP;
        statusBarView.setLayoutParams(params);
        statusBarView.setBackgroundColor(statusBarColor);
        statusBarView.setTag(TAG_FAKE_STATUS_BAR_VIEW);
        decorView.addView(statusBarView);
    }
總算大功告成,接着看看實際的運行效果,具體如下圖所示。由於上述代碼同時兼容Android4.4,以及5.0以上版本這兩種情況,因此就不重複貼圖了。其中左圖爲懸浮狀態欄的效果圖,右圖爲恢復狀態欄的效果圖。




點此查看Android開發筆記的完整目錄


__________________________________________________________________________
本文現已同步發佈到微信公衆號“老歐說安卓”,打開微信掃一掃下面的二維碼,或者直接搜索公衆號“老歐說安卓”添加關注,更快更方便地閱讀技術乾貨。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章