1. 程式人生 > >透明狀態列和導航欄的終極解決方案

透明狀態列和導航欄的終極解決方案

背景

在我做 Android 開發之前,我就發現有些 App 的狀態列和導航欄有透明效果,或者是沉浸式效果,比如說酷安的客戶端,是像這個樣子的


酷安客戶端

雖然只是簡單的改變,但相對於傳統的上下兩個黑條來說,視覺效果會美觀很多,我當時挺糾結很多主流應用沒有這種效果,還特意安裝了一個 xposed 框架的模組來強制實現沉浸式狀態列和導航欄,不過貌似那個模組會影響效能,從那時我就決定,如果將來我做 Android 開發,一定會讓我開發的應用都使用這種效果,如今終於實現啦!

開源庫

經過對大量應用的觀察,我發現這種透明狀態列和導航欄或者叫沉浸式狀態列和導航欄的效果主要有以下幾種:
1、自定義顏色的狀態列和導航欄;
2、半透明的狀態列和導航欄;
3、完全透明的狀態列和導航欄(其實就是第二種的極限狀態,我更喜歡 叫這種為沉浸式狀態列和導航欄);
4、隱藏狀態列和導航欄。

效果分別如下:


自定義顏色

半透明
完全透明
隱藏

事實上,在 github 上也有不少關於這方面的開源專案,不過這些開源專案大多隻是針對狀態列實現了透明或者沉浸式的效果,而對下方的導航欄並沒有做相應的處理,於是我自己寫了一個針對狀態列和導航欄都實現透明或者沉浸式的效果的開源庫,地址如下:

這裡要特別說明一下,狀態列和導航欄透明是在 Android 4.4 開始支援的,但是 Android 4.4 的實現原理和 Android 5.0 以上的實現原理並不一樣,這就導致如果在 Android 5.0 以上如果使用 Android 4.4 的實現方法會出現顯示效果不一致的問題,我寫的這個庫分別對 Android 4.4 和 Android5.0 以上做了處理,使它在不同的系統版本下顯示效果達到高度統一,使用這個庫,首先需要新增依賴:

compile 'org.zackratos:ultimatebar:1.0.3'

接下來對上面四種情況分別作介紹。

自定義顏色的狀態列和導航欄

要設定自定義顏色的狀態列和導航欄只需要在 onCreate 方法中呼叫如下程式碼:

UltimateBar ultimateBar = new UltimateBar(this);
ultimateBar.setColorBar(ContextCompat.getColor(this, R.color.DeepSkyBlue));

那麼他的內部是怎麼實現的呢,檢視原始碼可以發現,內部原始碼是這樣的:

@TargetApi(Build.VERSION_CODES.KITKAT)
public
void setColorBar(@ColorInt int color, int alpha)
{ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); int alphaColor = alpha == 0 ? color : calculateColor(color, alpha); window.setStatusBarColor(alphaColor); window.setNavigationBarColor(alphaColor); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { Window window = activity.getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); int alphaColor = alpha == 0 ? color : calculateColor(color, alpha); ViewGroup decorView = (ViewGroup) window.getDecorView(); decorView.addView(createStatusBarView(activity, alphaColor)); if (navigationBarExist(activity)) { decorView.addView(createNavBarView(activity, alphaColor)); window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); } setRootView(activity, true); } } @TargetApi(Build.VERSION_CODES.KITKAT) public void setColorBar(@ColorInt int color) { setColorBar(color, 0); }

我們可以看到第一個方法裡面傳入了兩個引數,第一個引數是自定義的顏色值,第二個引數是顏色深度值,最小為 0,最大為 255,當深度值為 0 時,狀態列和導航欄的顏色就是第一個引數傳入的顏色值,即為第二個方法中的情況;當深度值不為 0 時,會根據深度值計算得到最終的顏色值,然後設定到狀態列和導航欄上面。

正如前面所說,這裡分別針對 Android 4.4 和 Android 5.0 以上做了不同處理,首先來看 Android 5.0 以上的情況,事實上 Android 5.0 以上的實現非常簡單,因為 Android 5.0 以上可以直接設定狀態列和導航欄的顏色,所以只需要先得到最終的顏色值,然後呼叫 setStatusBarColor 和 setNavigationBarColor 方法進行設定就好了。然後 Android 4.4 稍微麻煩一點,首先必須要新增 FLAG_TRANSLUCENT_STATUS 這個 flag 來把狀態列設定為透明,然後再在狀態列上面新增一個 view 來保證狀態列的顏色,然後再呼叫 navigationBarExist 方法來判斷當前手機是否存在導航欄,如果存在,對導航欄做同樣的處理,最後必須呼叫 setRootView 方法,這個方法是幹嘛的呢,看一下它的程式碼:

private void setRootView(Activity activity, boolean fit) {
    ViewGroup parent = (ViewGroup) activity.findViewById(android.R.id.content);
    for (int i = 0, count = parent.getChildCount(); i < count; i++) {
        View childView = parent.getChildAt(i);
        if (childView instanceof ViewGroup) {
            childView.setFitsSystemWindows(fit);
            ((ViewGroup)childView).setClipToPadding(fit);
        }
    }
}

可以看到,這個方法是用來設定佈局的子 view 的 fitsSystemWindows 引數的,相當於在佈局中新增 android:fitsSystemWindows="true",如果不呼叫這個方法,就會導致佈局中的內容覆蓋到狀態列和導航欄上面了。

半透明的狀態列和導航欄

半透明狀態列和導航欄的使用方法也非常簡單,只要在 onCreate 方法中呼叫以下程式碼:

UltimateBar ultimateBar = new UltimateBar(this);
ultimateBar.setTransparentBar(Color.BLUE, 50);

同樣的看一下它的內部實現,如下:

@TargetApi(Build.VERSION_CODES.KITKAT)
public void setTransparentBar(@ColorInt int color, int alpha) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        View decorView = window.getDecorView();
        int option = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
        decorView.setSystemUiVisibility(option);

        int finalColor = alpha == 0 ? Color.TRANSPARENT :
                Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));

        window.setNavigationBarColor(finalColor);
        window.setStatusBarColor(finalColor);

    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
        Window window = activity.getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        int finalColor = alpha == 0 ? Color.TRANSPARENT :
                Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
        decorView.addView(createStatusBarView(activity, finalColor));
        if (navigationBarExist(activity)) {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            decorView.addView(createNavBarView(activity, finalColor));
        }
    }

}

兩個引數分別表示顏色和透明度,透明度最小為 0,最大為 255,對於 Android 5.0 及以上,需要先新增 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,SYSTEM_UI_FLAG_LAYOUT_STABLE 三個 flag,以保證佈局的內容可以覆蓋到狀態列和導航欄上面,然後同樣的呼叫 setStatusBarColor 和 setNavigationBarColor 方法來設定狀態列和導航欄顏色,不過這裡的顏色都是經過計算的半透明的顏色,對於 Android 4.4,跟之前的自定義顏色一樣,首先需要新增 FLAG_TRANSLUCENT_STATUS 這個 flag 保證狀態列透明,然後再在狀態列上新增一個半透明的 view,然後呼叫 navigationBarExist 方法判斷導航欄是否存在,如果存在,也做相同的處理,這裡要注意,因為半透明狀態列和導航欄需要佈局內容覆蓋到狀態列和導航欄上面的效果,所以在這裡不能呼叫 setRootView 方法。

完全透明的狀態列和導航欄

其實完全透明的狀態列和導航欄就是半透明的狀態列和導航欄中當透明度為 0 的情況,只需在 onCreate 方法中呼叫如下方法:

UltimateBar ultimateBar = new UltimateBar(this);
ultimateBar.setImmersionBar();

檢視它的內部實現可以發現它是這麼呼叫的:

@TargetApi(Build.VERSION_CODES.KITKAT)
public void setImmersionBar() {
    setTransparentBar(Color.TRANSPARENT, 0);
}

就是半透明狀態列和導航欄的特殊情況,不做過多介紹了。

隱藏狀態列和導航欄

這種情況比較常見了,一般玩遊戲,看視訊就是這種效果,這種效果的實現有點特殊,必須重寫 Activity 的 onWindowFocusChanged 方法,如下:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        UltimateBar ultimateBar = new UltimateBar(this);
        ultimateBar.setHintBar();
    }
}

它的內部實現也比較簡單,如下:

@TargetApi(Build.VERSION_CODES.KITKAT)
public void setHintBar() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        View decorView = activity.getWindow().getDecorView();
        decorView.setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
    }
}

就是新增幾個 flag,這是固定寫法,也不做過多介紹了。

針對 DrawerLayout 的實現

還有一種特殊情況,就是對於 DrawerLayout,上面的方法會出現一些問題,達不到想要的效果,這裡針對 DrawerLayout 做了特殊處理,一般來說,對於 DrawerLayout 只要實現自定義顏色的狀態列和導航欄效果就好了,其他情況就不用考慮了,可以在 onCrate 呼叫如下程式碼:

UltimateBar ultimateBar = new UltimateBar(this);
ultimateBar.setColorBarForDrawer(ContextCompat.getColor(this, R.color.DeepSkyBlue));

但是這樣其實還是不夠的,還必須要在佈局檔案中在 DawerLayout 的子 view 的主介面新增 android:fitsSystemWindows="true",就像這樣:

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        android:orientation="vertical">
    </LinearLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/SpringGreen"
        android:layout_gravity="left"/>

</android.support.v4.widget.DrawerLayout>

注意,這裡是在 DawerLayout 下面的主介面新增,DawerLayout 本身以及它下面的抽屜都不能新增,原因後面會說明,然後同樣看一下內部實現:

@TargetApi(Build.VERSION_CODES.KITKAT)
public void setColorBarForDrawer(@ColorInt int color, int alpha) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = activity.getWindow();
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
        if (navigationBarExist(activity)) {
            option = option | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
        }
        decorView.setSystemUiVisibility(option);
        window.setNavigationBarColor(Color.TRANSPARENT);
        window.setStatusBarColor(Color.TRANSPARENT);
        int alphaColor = alpha == 0 ? color : calculateColor(color, alpha);
        decorView.addView(createStatusBarView(activity, alphaColor), 0);
        if (navigationBarExist(activity)) {
            decorView.addView(createNavBarView(activity, alphaColor), 1);
        }
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        Window window = activity.getWindow();
        window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        int alphaColor = alpha == 0 ? color : calculateColor(color, alpha);
        decorView.addView(createStatusBarView(activity, alphaColor), 0);
        if (navigationBarExist(activity)) {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
            decorView.addView(createNavBarView(activity, alphaColor), 1);
        }
    }
}



@TargetApi(Build.VERSION_CODES.KITKAT)
public void setColorBarForDrawer(@ColorInt int color) {
    setColorBarForDrawer(color, 0);
}

這裡稍微有點複雜,引數的傳遞和前面是一樣的,就不多解釋了,對於 Android 5.0 以上的情況,首先新增前面兩個 flag 保證佈局內容能夠覆蓋到狀態列上面,然後判斷是否存在導航欄,如果存在,再新增第三個 flag 保證佈局內容可以覆蓋到導航欄上面,然後狀態列和導航欄都設為透明色,此時相當於上面的完全透明的狀態列和導航欄,最後再分別在狀態列和導航欄上面新增一個 view 保證狀態列和導航欄有顏色,這樣就既保證了 DrawerLayout 可以覆蓋到狀態列和導航欄上面,又保證了 DrawerLayout 下面的主佈局內容不會覆蓋到狀態列和導航欄上面,最後的效果就是抽屜的內容是覆蓋到狀態列和導航欄上面的,而住佈局的內容不會覆蓋到狀態列和導航欄的上面,然後對於 Android 4.4,其實和前面正常情況的設定自定義顏色的狀態列和導航欄是一樣的,只是這裡沒有呼叫 setRootView 方法,而是在 DrawerLayout 下面的主佈局中添加了 android:fitsSystemWindows="true",同樣實現了抽屜的內容可以覆蓋到狀態列和導航欄上面,而主佈局的內容不會覆蓋到狀態列和導航欄上面,最後的效果如下圖


抽屜未抽出
抽屜抽出一半
抽屜完全抽出

大致內容也就這麼多了,最後再把這個庫的地址貼一遍:

如果覺得這個庫對你的開發有幫助,歡迎 star,歡迎 fork,如果發現有什麼問題或者有什麼修改建議,歡迎反饋,謝謝!