Android 透明狀態欄

weijh 8年前發布 | 40K 次閱讀 Android開發 移動開發 Android

最近業務上看到一個設計圖挺好看,所以研究了一下透明狀態欄,注意不是沉浸式狀態欄,在參考了網上的一些資料后,整理出了這篇博客.

Github Demo 鏈接: StatusBarCompat

參考文章:

  1. 由沉浸式狀態欄引發的血案

  2. Translucent System Bar 的最佳實踐

  3. 該使用 fitsSystemWindows 了!

首先強調,對于狀態欄的處理有兩種不同的方式, 這里從Translucent System Bar 的最佳實踐直接盜了兩張圖做對比~.

全屏( ContentView 可以進入狀態欄) 非全屏 ( ContentView 與狀態欄分離, 狀態欄直接著色)

先定義幾個名詞:

  1. 全屏模式: 左邊圖所示.

  2. 著色模式: 右邊圖所示.

  3. ContentView: activity.findViewById(Window.ID_ANDROID_CONTENT) 獲取的 View , 即 setContentView 方法所設置的 View, 實質為 FrameLayout.

  4. ContentParent: ContentView 的 parent , 實質為 LinearLayout.

  5. ChildView: ContentView 的第一個子 View ,即布局文件中的 layout .

再介紹一下相關的函數:

  1. fitsSystemWindows, 該屬性可以設置是否為系統 View 預留出空間, 當設置為 true 時,會預留出狀態欄的空間.

  2. ContentView, 實質為 ContentFrameLayout, 但是重寫了 dispatchFitSystemWindows 方法, 所以對其設置 fitsSystemWindows 無效.

  3. ContentParent, 實質為 FitWindowsLinearLayout, 里面第一個 View 是 ViewStubCompat, 如果主題沒有設置 title ,它就不會 inflate .第二個 View 就是 ContentView.

5.0以上的處理:

自5.0引入 Material Design ,狀態欄對開發者更加直接,可以直接調用 setStatusBarColor 來設置狀態欄的顏色.

全屏模式:

Window window = activity.getWindow();
//設置透明狀態欄,這樣才能讓 ContentView 向上
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 

//需要設置這個 flag 才能調用 setStatusBarColor 來設置狀態欄顏色
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 
//設置狀態欄顏色
window.setStatusBarColor(statusColor);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    //注意不是設置 ContentView 的 FitsSystemWindows, 而是設置 ContentView 的第一個子 View . 使其不為系統 View 預留空間.
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

著色模式:

Window window = activity.getWindow();
//取消設置透明狀態欄,使 ContentView 內容不再覆蓋狀態欄
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 

//需要設置這個 flag 才能調用 setStatusBarColor 來設置狀態欄顏色
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 
//設置狀態欄顏色
window.setStatusBarColor(statusColor);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    //注意不是設置 ContentView 的 FitsSystemWindows, 而是設置 ContentView 的第一個子 View . 預留出系統 View 的空間.
    ViewCompat.setFitsSystemWindows(mChildView, true);
}

4.4-5.0的處理:

4.4-5.0因為沒有直接的 API 可以調用,需要自己兼容處理,網上的解決方法基本都是創建一下高度為狀態欄的 View ,通過設置這個 View 的背景色來模擬狀態欄. 這里我嘗試了三種方法來兼容處理.

方法1: 向 ContentView 添加假 View , 設置 ChildView 的 marginTop 屬性來模擬 fitsSystemWindows .

全屏模式:

Window window = activity.getWindow();
ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);

//首先使 ChildView 不預留空間
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

int statusBarHeight = getStatusBarHeight(activity);
//需要設置這個 flag 才能設置狀態欄
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
//避免多次調用該方法時,多次移除了 View
if (mChildView != null && mChildView.getLayoutParams() != null && mChildView.getLayoutParams().height == statusBarHeight) {
    //移除假的 View.
    mContentView.removeView(mChildView);
    mChildView = mContentView.getChildAt(0);
}
if (mChildView != null) {
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mChildView.getLayoutParams();
    //清除 ChildView 的 marginTop 屬性
    if (lp != null && lp.topMargin >= statusBarHeight) {
        lp.topMargin -= statusBarHeight;
        mChildView.setLayoutParams(lp);
    }
}

著色模式:

Window window = activity.getWindow();
ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);

//First translucent status bar.
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
int statusBarHeight = getStatusBarHeight(activity);

View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mChildView.getLayoutParams();
    //如果已經為 ChildView 設置過了 marginTop, 再次調用時直接跳過
    if (lp != null && lp.topMargin < statusBarHeight && lp.height != statusBarHeight) {
        //不預留系統空間
        ViewCompat.setFitsSystemWindows(mChildView, false); 
        lp.topMargin += statusBarHeight;
        mChildView.setLayoutParams(lp);
    }
}

View statusBarView = mContentView.getChildAt(0);
if (statusBarView != null && statusBarView.getLayoutParams() != null && statusBarView.getLayoutParams().height == statusBarHeight) {
    //避免重復調用時多次添加 View
    statusBarView.setBackgroundColor(statusColor);
    return;
}
statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, statusBarHeight);
statusBarView.setBackgroundColor(statusColor);
//向 ContentView 中添加假 View
mContentView.addView(statusBarView, 0, lp);

方法2: 向 ContentParent 添加假 View ,設置 ContentView 和 ChildView 的 fitsSystemWindows.

全屏模式:

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
ViewGroup mContentParent = (ViewGroup) mContentView.getParent();

View statusBarView = mContentParent.getChildAt(0);
if (statusBarView != null && statusBarView.getLayoutParams() != null && statusBarView.getLayoutParams().height == getStatusBarHeight(activity)) {
    //移除假的 View
    mContentParent.removeView(statusBarView);
}
//ContentView 不預留空間
if (mContentParent.getChildAt(0) != null) {
    ViewCompat.setFitsSystemWindows(mContentParent.getChildAt(0), false);
}

//ChildView 不預留空間
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

著色模式(會有一條黑線,無法解決):

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
ViewGroup mContentParent = (ViewGroup) mContentView.getParent();

View statusBarView = mContentParent.getChildAt(0);
if (statusBarView != null && statusBarView.getLayoutParams() != null && statusBarView.getLayoutParams().height == getStatusBarHeight(activity)) {
    //避免重復調用時多次添加 View
    statusBarView.setBackgroundColor(statusColor);
    return;
}

//創建一個假的 View, 并添加到 ContentParent
statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
        getStatusBarHeight(activity));
statusBarView.setBackgroundColor(statusColor);
mContentParent.addView(statusBarView, 0, lp);

//ChildView 不需要預留系統空間
View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

方法3:向 ContentView 添加假 View , 設置 ChildView 的 fitsSystemWindows.

全屏模式:

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
View statusBarView = mContentView.getChildAt(0);
//移除假的 View
if (statusBarView != null && statusBarView.getLayoutParams() != null && statusBarView.getLayoutParams().height == getStatusBarHeight(activity)) {
    mContentView.removeView(statusBarView);
}
//不預留空間
if (mContentView.getChildAt(0) != null) {
    ViewCompat.setFitsSystemWindows(mContentView.getChildAt(0), false);
}

著色模式:

Window window = activity.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

ViewGroup mContentView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
int statusBarHeight = getStatusBarHeight(activity);

View mTopView = mContentView.getChildAt(0);
if (mTopView != null && mTopView.getLayoutParams() != null && mTopView.getLayoutParams().height == statusBarHeight) {
    //避免重復添加 View
    mTopView.setBackgroundColor(statusColor);
    return;
}
//使 ChildView 預留空間
if (mTopView != null) {
    ViewCompat.setFitsSystemWindows(mTopView, true);
}

//添加假 View
mTopView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, statusBarHeight);
mTopView.setBackgroundColor(statusColor);
mContentView.addView(mTopView, 0, lp);

 

其實全屏模式在三種模式下實現都是一樣的,主要是著色模式實現不同.

對比一下三種著色模式實現的方式:

  方法1 方法2 方法3
原理 向 ContentView 中添加假 View, 然后利用 ChildView 的 marginTop 屬性來模擬 fitsSystemWindows ,主要是通過修改 marginTop 的值可以在全屏模式和著色模式之間切換. 因為 ParentView 的實質是一個 LinearLayout , 可以再其頂部添加 View . 向 ContentView 中添加假 View, 然后利用ChildView 的 fitsSystemWindows 屬性來控制位置, 但是實現缺陷就是不能隨時切換兩種模式.
缺陷 改變了 ChildView 的 marginTop 值 著色模式下,會像由沉浸式狀態欄引發的血案中一樣出現一條黑線 不能在不重啟 Activity 的情況下切換模式.
對應 Github demo 中代碼 StatusBarCompat類 StatusBarCompat1類 StatusBarCompat2 類

總結

  • StatusBarCompat2 主要問題不能切換.

  • StatusBarCompat1 在4.4上會有一條黑線, 如果可以解決我覺得這是最靠譜的解決方法.

  • StatusBarCompat 類算是我最后給出的解決方案吧, 目前使用效果比較完善.推薦使用

    • 用戶可以隨時在同一個 Activity 中切換不同的狀態欄模式.

    • 就算子 View 重寫了 dispatchFitSystemWindows 也不會有影響.

 

 

來源:Jianqiu's blog

  •  

 

 

 本文由用戶 weijh 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!