Android 沉浸式狀態欄的實現

一提到沉浸式狀態欄,第一個浮現在腦海裏的詞就是“碎片化”。碎片化是讓 Android 開發者很頭疼的問題,相信沒有哪位開發者會不喜歡“write once, run anywhere”的感覺,碎片化讓我們不得不耗費精力去校驗代碼在各個系統版本、各個機型上是否有效。因此以前我一直把沉浸式狀態欄看作一塊難啃的骨頭,但是該面對的問題遲早還是要面對,所以,不如就此開始吧。

沉浸式狀態欄的實現

方法一:通過設置 Theme 主題設置狀態欄透明

因爲 API21 之後(也就是 android 5.0 之後)的狀態欄,會默認覆蓋一層半透明遮罩。且爲了保持4.4以前系統正常使用,故需要三份 style 文件,即默認的values(不設置狀態欄透明)、values-v19、values-v21(解決半透明遮罩問題)。

//valuse
<style name="TranslucentTheme" parent="AppTheme">
</style>

// values-v19。v19 開始有 android:windowTranslucentStatus 這個屬性
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
</style>

// values-v21。5.0 以上提供了 setStatusBarColor()  方法設置狀態欄顏色。
<style name="TranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowTranslucentStatus">false</item>
    <item name="android:windowTranslucentNavigation">true</item>
    <!--Android 5.x開始需要把顏色設置透明,否則導航欄會呈現系統默認的淺灰色-->
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

設置狀態欄爲透明

由圖可見,設置之後佈局的內容延伸到了狀態欄。但有些場景下,我們還是需要狀態欄那塊位置存在的(然而不存在的)。有三種解決方法:

法一:設置 fitsSystemWindows 屬性

引用一下官方對該屬性的解釋吧:

android:fitsSystemWindows

Boolean internal attribute to adjust view layout based on system windows such as the status bar. If true, adjusts the padding of this view to leave space for the system windows. Will only take effect if this view is in a non-embedded activity.

當該屬性設置 true 時,會在屏幕最上方預留出狀態欄高度的 padding。

在佈局的最外層設置 android:fitsSystemWindows="true" 屬性。當然,也可以通過代碼設置:


/**
* 設置頁面最外層佈局 FitsSystemWindows 屬性
* @param activity
* @param value
*/
public static void setFitsSystemWindows(Activity activity, boolean value) {
ViewGroup contentFrameLayout = (ViewGroup) activity.findViewById(android.R.id.content);
View parentView = contentFrameLayout.getChildAt(0);
if (parentView != null && Build.VERSION.SDK_INT >= 14) {
parentView.setFitsSystemWindows(value);
}
}

通過該設置保留狀態欄高度的 paddingTop 後,再設置狀態欄的顏色。就可以達到設想的效果。但這種方式實現有些問題,例如我們想設置狀態欄爲藍色,只能通過設置最外層佈局的背景爲藍色來實現,然而一旦設置後,整個佈局就都變成了藍色,只能在下方的佈局內容裏另外再設置白色背景,而這樣就存在過度繪製了。而且設置了 fitsSystemWindows=true 屬性的頁面,在點擊 EditText 調出 軟鍵盤時,整個視圖都會被頂上去。

法二:佈局裏添加佔位狀態欄

法一:在根佈局加入一個佔位狀態欄,這樣雖然整個內容頁面時頂到頭的,但是因爲在內容佈局裏添加了一個佔位狀態欄,所以效果與設想的一致。


<View
android:id="@+id/statusBarView"
android:background="@color/blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"></View>

通過反射獲取狀態欄高度:


/**
* 利用反射獲取狀態欄高度
* @return
*/
public int getStatusBarHeight() {
int result = 0;
//獲取狀態欄高度的資源id
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}

設置佔位視圖高度


View statusBar = findViewById(R.id.statusBarView);
ViewGroup.LayoutParams layoutParams = statusBar.getLayoutParams();
layoutParams.height = getStatusBarHeight();

當然,除了從佈局文件中添加這一方式之外,一樣可以在代碼中添加。比較推薦使用代碼添加的方式,方便封裝使用。

/**
 * 添加狀態欄佔位視圖
 *
 * @param activity
 */
private void addStatusViewWithColor(Activity activity, int color) {
    ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content);    
    View statusBarView = new View(activity);
    ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            getStatusBarHeight(activity));
    statusBarView.setBackgroundColor(color);
    contentView.addView(statusBarView, lp);
}

添加佔位狀態欄

法三:代碼中設置 paddingTop 並添加佔位狀態欄

手動給根視圖設置一個 paddingTop ,高度爲狀態欄高度,相當於手動實現了 fitsSystemWindows=true 的效果,然後再在根視圖加入一個佔位視圖,其高度也設置爲狀態欄高度。

//設置 paddingTop
ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
rootView.setPadding(0, getStatusBarHeight(mActivity), 0, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    //5.0 以上直接設置狀態欄顏色
    activity.getWindow().setStatusBarColor(color);
} else {
    //根佈局添加佔位狀態欄
    ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
    View statusBarView = new View(activity);
    ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
            getStatusBarHeight(activity));
    statusBarView.setBackgroundColor(color);
    decorView.addView(statusBarView, lp);
}

個人認爲最優解應該是第三種方法,通過這種方法達到沉浸式的效果後面也可以很方便地拓展出漸變色的狀態欄。

方法二:代碼中設置

通過在代碼中設置,實現方法一中在 Theme 主題樣式裏設置的屬性,便於封裝。

 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    int flagTranslucentStatus = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
    int flagTranslucentNavigation = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = getWindow();
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.flags |= flagTranslucentNavigation;
        window.setAttributes(attributes);
        getWindow().setStatusBarColor(Color.TRANSPARENT);
    } else {
        Window window = getWindow();
        WindowManager.LayoutParams attributes = window.getAttributes();
        attributes.flags |= flagTranslucentStatus | flagTranslucentNavigation;
        window.setAttributes(attributes);
    }
}

但是從圖片中也看到了,該方案會導致一個問題就是導航欄顏色變灰。
經測試,在 5.x 以下導航欄透明是可以生效的,但 5.x 以上導航欄會變灰色(正常情況下我們期望導航欄保持默認顏色黑色不變),但因爲設置了FLAG_TRANSLUCENT_NAVIGATION,所以即使代碼中設置 getWindow().setNavigationBarColor(Color.BLACK); 也是不起作用的。但如果不設置該 FLAG ,狀態欄又無法被置爲隱藏和設置透明。

方案二:全屏模式的延伸

通過設置 FLAG ,讓應用內容佔用系統狀態欄的空間,經測試該方式不會影響對導航欄的設置。

/**
 * 通過設置全屏,設置狀態欄透明
 *
 * @param activity
 */
private void fullScreen(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            //5.x開始需要把顏色設置透明,否則導航欄會呈現系統默認的淺灰色
            Window window = activity.getWindow();
            View decorView = window.getDecorView();
            //兩個 flag 要結合使用,表示讓應用的主體內容佔用系統狀態欄的空間
            int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
            decorView.setSystemUiVisibility(option);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
            //導航欄顏色也可以正常設置
//                window.setNavigationBarColor(Color.TRANSPARENT);
        } else {
            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);
        }
    }
}

可以正常設置導航欄顏色

驗證其他使用場景

側滑菜單

使用 AS 自動創建 Navigation Drawer Activity ,佈局結構爲:

  • DrawerLayout
    • include :內容佈局,默認使用 ToolBar
    • NavigationView :側滑佈局

這裏只調用了 fullScreen(), 測試一下運行結果如何:

側滑菜單

可以看到都有不盡如人意的地方,4.4 系統中內容視圖是可以正常延伸到狀態欄中,但側滑菜單中卻在上方出現了白條,而在 6.0 中側滑菜單上會有半透明遮罩。針對 6.0 側滑菜單半透明遮罩問題,通過設置爲 NavigationView 設置屬性 app:insetForeground="#00000000" 即可解決。針對 4.4 側滑菜單白條問題,經過測試,通過對最外層佈局設置 setFitsSystemWindows(true)setClipToPadding(false) 可以解決,所以這裏對之前的 fitsSystemWindows 方法稍加修改:

 /**
 * 設置頁面最外層佈局 FitsSystemWindows 屬性
 *
 * @param activity
 */
private void fitsSystemWindows(Activity activity) {
    ViewGroup contentFrameLayout = (ViewGroup) activity.findViewById(android.R.id.content);
    View parentView = contentFrameLayout.getChildAt(0);
    if (parentView != null && Build.VERSION.SDK_INT >= 14) {
        //佈局預留狀態欄高度的 padding
        parentView.setFitsSystemWindows(true);
        if (parentView instanceof DrawerLayout) {
            DrawerLayout drawer = (DrawerLayout) parentView;
            //將主頁面頂部延伸至status bar;雖默認爲false,但經測試,DrawerLayout需顯示設置
            drawer.setClipToPadding(false);
        }
    }
}

這樣是解決了上述的問題,既然延伸內容沒問題了,那就開開心心地像上面一樣調用 addStatusViewWithColor() 方法增加個佔位狀態欄,解決一下內容頂到頭的問題吧:

4.4 系統,增加佔位狀態欄異常

可以看到,效果依然不是我們想要的,雖然佔位狀態欄是有了,但是卻也覆蓋到了側滑菜單上,並且即使設置了 android:fitsSystemWindows="true" 也並沒有什麼卵用,內容佈局依然頂到了頭部。這裏有兩種解決方法:1. 第一種方案是網上提到比較多的,改變 ToolBar 的高度,並增加狀態欄高度的 paddingTop,這也是
ImmersionBar 庫採用的方案。2. 第二種方案其實思路與第一種差不多,就是將原有的內容佈局從 DrawerLayout 中移除,並添加到線性佈局(佈局中已有佔位狀態欄),之後再將這個線性佈局添加到 DrawerLayout 中成爲新的內容佈局,此謂狸貓換太子。

/**
 * 是否是最外層佈局爲 DrawerLayout 的側滑菜單
 * @param drawerLayout 是否最外層佈局爲 DrawerLayout
 * @param contentId 內容視圖的 id
 * @return
 */
public StatusBarUtils setIsDrawerLayout(boolean drawerLayout, int contentId) {
    mIsDrawerLayout = drawerLayout;
    mContentResourseIdInDrawer = contentId;
    return this;
}

/**
 * 添加狀態欄佔位視圖
 *
 * @param activity
 */
private void addStatusViewWithColor(Activity activity, int color) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        if (isDrawerLayout()) {
            //要在內容佈局增加狀態欄,否則會蓋在側滑菜單上
            ViewGroup rootView = (ViewGroup) activity.findViewById(android.R.id.content);
            //DrawerLayout 則需要在第一個子視圖即內容試圖中添加padding
            View parentView = rootView.getChildAt(0);
            LinearLayout linearLayout = new LinearLayout(activity);
            linearLayout.setOrientation(LinearLayout.VERTICAL);
            View statusBarView = new View(activity);
            ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    getStatusBarHeight(activity));
            statusBarView.setBackgroundColor(color);
            //添加佔位狀態欄到線性佈局中
            linearLayout.addView(statusBarView, lp);
            //側滑菜單
            DrawerLayout drawer = (DrawerLayout) parentView;
            //內容視圖
            View content = activity.findViewById(mContentResourseIdInDrawer);
            //將內容視圖從 DrawerLayout 中移除
            drawer.removeView(content);
            //添加內容視圖
            linearLayout.addView(content, content.getLayoutParams());
            //將帶有佔位狀態欄的新的內容視圖設置給 DrawerLayout
            drawer.addView(linearLayout, 0);
        } else {
            //設置 paddingTop
            ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
            rootView.setPadding(0, getStatusBarHeight(mActivity), 0, 0);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                //直接設置狀態欄顏色
                activity.getWindow().setStatusBarColor(color);
            } else {
                //增加佔位狀態欄
                ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
                View statusBarView = new View(activity);
                ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        getStatusBarHeight(activity));
                statusBarView.setBackgroundColor(color);
                decorView.addView(statusBarView, lp);
            }
        }
    }
}

一番操作後,效果如下:

改進 addStatusViewWithColor() 後的效果

對於內容視圖未使用到 ToolBar 的情況方案二依然可以適用。

ActionBar

上述代碼在使用 ActionBar 時可以完美適配嗎?測試後效果如下圖所示

6.0 狀態欄黑邊

可以看到,通過添加指定顏色的佔位狀態來達到沉浸效果的方案,在 4.4 系統上效果是正常的,但是在 6.0 上,在狀態欄和 Actionbar 之間會有陰影,這個陰影是主題的效果。不知道大家還記不記得 Theme 主題裏的幾個設計顏色的屬性:

各屬性顏色

colorPrimary 指定 ActionBar 的顏色,colorPrimaryDark 指定狀態欄顏色,經過測試,在主題裏將二者設爲統一顏色,狀態欄和 ActionBar 之間不會有黑邊。自然,我們除了在 Theme 主題裏設置,還可以直接在代碼裏通過上文提到過的代碼修改 5.x 以上系統的狀態欄顏色:

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.BLUE);

但是因爲 setStatusBarColor() 方法的參數無法傳入 Drawble ,所以這種方式是無法實現漸變色狀態欄的效果的。所以還是應該聚焦在怎麼解決 ActionBar 陰影的問題,上面說了,既然這個陰影是 Theme 的效果,那就肯定有移除這種效果的方法,一種解決方法是更改主題爲 ActionBar 不帶陰影的主題樣式:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:windowContentOverlay">@null</item>
    //更改 ActionBar 風格樣式
    <item name="actionBarStyle">@style/ActionBarStyleWithoutShadow</item>
</style>

//ActionBar 不帶陰影的主題樣式
<style name="ActionBarStyleWithoutShadow" parent="android:Theme.Holo.ActionBar">
    <item name="background">@color/blue</item>
</style>

還有第二種更簡單的方式,那就是直接在代碼裏設置去除陰影:

/**
 * 去除 ActionBar 陰影
 */
public StatusBarUtils clearActionBarShadow() {
    if (Build.VERSION.SDK_INT >= 21) {
        ActionBar supportActionBar = ((AppCompatActivity) mActivity).getSupportActionBar();
        if (supportActionBar != null) {
            supportActionBar.setElevation(0);
        }
    }
    return this;
}

並且因爲內容是位於 ActionBar 之下的,我們還必須給內容視圖是指一個 paddingTop,高度爲狀態欄高度+ActionBar 高度,纔可以使內容正常顯示。我們給 ActionBar 設置一個漸變色試試看:

//drawble 文件夾內新建 shape 漸變色
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="0"
        android:centerX="0.7"
        android:endColor="@color/shape2"
        android:startColor="@color/shape1"
        android:centerColor="@color/shape3"
        android:type="linear" />
</shape>

//ActionBar 設置漸變背景色
getSupportActionBar().setBackgroundDrawable(getResources().getDrawable(R.drawable.shape));

//佔位狀態欄 設置漸變背景色
View statusBarView = new View(activity);
...
//增加佔位狀態欄方法同上,只是在設置 statusBarView 背景上有 color 和 drawble 之分
statusBarView.setBackground(drawable);

if (isActionBar()) {
    //要增加內容視圖的 paddingTop,否則內容被 ActionBar 遮蓋
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        ViewGroup rootView = (ViewGroup) mActivity.getWindow().getDecorView().findViewById(android.R.id.content);
        rootView.setPadding(0, getStatusBarHeight(mActivity) + getActionBarHeight(mActivity), 0, 0);
    }
}

漸變色狀態欄

至此,嘗試適配了幾種比較常見的使用場景的沉浸式狀態欄,效果也都還比較符合預期。真正去處理這個問題時會發現其實問題也沒有想象中的那麼複雜。最後附上 Github 源碼

Stay hungry. Stay foolish.

下篇博客再見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章