Android 状态栏最佳实践

9,246 阅读3分钟
原文链接: niorgai.github.io

本文来自Jianqiu's blog

最近业务上看到一个设计图挺好看,所以研究了一下透明状态栏,注意不是沉浸式状态栏,在参考了网上的一些资料后,整理出了这篇博客.

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();

window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 


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) {
    
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

着色模式:

Window window = activity.getWindow();

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); 


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) {
    
    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);


View mChildView = mContentView.getChildAt(0);
if (mChildView != null) {
    ViewCompat.setFitsSystemWindows(mChildView, false);
}

int statusBarHeight = getStatusBarHeight(activity);

window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

if (mChildView != null && mChildView.getLayoutParams() != null && mChildView.getLayoutParams().height == statusBarHeight) {
    
    mContentView.removeView(mChildView);
    mChildView = mContentView.getChildAt(0);
}
if (mChildView != null) {
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mChildView.getLayoutParams();
    
    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);


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();
    
    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) {
    
    statusBarView.setBackgroundColor(statusColor);
    return;
}
statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, statusBarHeight);
statusBarView.setBackgroundColor(statusColor);

mContentView.addView(statusBarView, 0, lp);

方法2: 向 ContentParent 添加假 View ,设置 ContentViewChildView 的 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)) {
    
    mContentParent.removeView(statusBarView);
}

if (mContentParent.getChildAt(0) != null) {
    ViewCompat.setFitsSystemWindows(mContentParent.getChildAt(0), false);
}


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)) {
    
    statusBarView.setBackgroundColor(statusColor);
    return;
}


statusBarView = new View(activity);
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
        getStatusBarHeight(activity));
statusBarView.setBackgroundColor(statusColor);
mContentParent.addView(statusBarView, 0, lp);


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);

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) {
    
    mTopView.setBackgroundColor(statusColor);
    return;
}

if (mTopView != null) {
    ViewCompat.setFitsSystemWindows(mTopView, true);
}


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, 然后利用 ChildViewfitsSystemWindows 属性来控制位置, 但是实现缺陷就是不能随时切换两种模式.
缺陷 改变了 ChildView 的 marginTop 值 着色模式下,会像由沉浸式状态栏引发的血案中一样出现一条黑线 不能在不重启 Activity 的情况下切换模式.
对应 Github demo 中代码 StatusBarCompat类 StatusBarCompat1类 StatusBarCompat2 类

总结

  • StatusBarCompat2 主要问题不能切换.
  • StatusBarCompat1 在4.4上会有一条黑线, 如果可以解决我觉得这是最靠谱的解决方法.
  • StatusBarCompat 类算是我最后给出的解决方案吧, 目前使用效果比较完善.推荐使用