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.

下篇博客再見。

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

推薦閱讀更多精彩內容