沉浸式状态栏实现及遇到的坑

Android4.4以前的版本,状态栏都是一块黑色的,个人认为还是比较丑的。自4.4开始,Android已经支持透明状态栏了(俗称沉浸式状态栏)。个人认为支持沉浸式状态栏的app逼格还是比较高的,为了紧跟潮流,我们项目中也准备加入沉浸式状态栏。在实现沉浸式状态栏的过程中踩了不少的坑,特此记录下来。

如何实现状态栏

Android 4.4以上实现方式

Android 4.4版本提供了FLAG_TRANSLUCENT_STATUS,在Activity中加入此flag,可以设置状态栏透明。代码如下:

1
2
3
4
5
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = getWindow();
// Translucent status bar
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}

仅仅设置FLAG_TRANSLUCENT_STATUS,你会发现界面上的ToolBar会跑到状态栏上面去,如下图:

通常我们会使用fitsSystemWindows属性来解决此问题。

  • fitSystemWindows官方描述:
    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.
  • 简单描述:
    这个属性的作用是让view可以根据系统窗口(如status bar)来调整自己的布局,如果值为true,就会调整view的paingding属性来给system windows留出空间(即给view添加一个值为状态栏高度的top padding)。

我们试着给ToolBar设置一下fitsSystemWindows属性为true。布局代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="example.com.chenjinshidemo.MainActivity">


<android.support.v7.widget.Toolbar
android:id="@+id/my_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />


<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="16dp">


<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#e0e0e0"
android:layout_gravity="bottom">

<EditText
android:layout_width="match_parent"
android:layout_height="40dp"
android:fitsSystemWindows="true"
android:background="@drawable/edit_text_rect_bg" />

</RelativeLayout>
</FrameLayout>
</LinearLayout>

4.4的效果图如下:

注:有些4.4的系统上面状态栏并不是全透明的,而是渐变的。

Android 5.0以上实现方式

你会发现,已经实现了沉浸式状态栏效果了。如果运行在5.0以上的机器上面,会发现大部分手机会出现状态栏是半透明的,效果图如下:

我们能不能让将5.0以上的手机也设置为和4.4一样的全透明的状态栏呢?答案是肯定的!Android自5.0起,又为我们提供了设置状态栏颜色的API,我们可以自己设置状态栏的颜色。
在代码中再加入如下代码:

1
2
3
4
5
6
7
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}

再在运行看看效果,状态栏已经变成全透明了。6.0运行效果图和上面4.4一样,就不再附图了。

Android 6.0以上设置状态栏字体颜色

默认状态栏字体颜色是白色的,如果ToolBar的颜色较浅,那么状态栏上白色的字看不怎么清楚。

Android6.0以后,我们可以使用代码将状态栏字体的颜色设置为黑色了,代码如下:

1
win.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);

设置了深色状态栏字体的效果图如下:

踩过的坑

如果你认为已经已经完美实现了,那真是too young to simple。下面是一些我踩过的坑。

与软键盘冲突的坑

如果在界面中有EditText的话,你会发现当软件盘弹出的时候(Activity已经设置了adjustResize),ToolBar的内容都被顶上去了,但是EditText输入框却被有顶上来(正常情况应该是ToolBar没事,输入框被软键盘顶上去),如下图:

这是为什么呢?经研究发现原来是fitsSystemWindows属性搞的鬼。哪个View设置了fitsSystemWindows=true,这个View就会被软件盘顶上去。所以说,fitsSystemWindows不能乱用,会有意想不到的坑。

那能不能不用fitsSystemWindows呢?既然上面说了,fitsSystemWindows=true的作用是给View添加值为状态栏高度的padding,那我们何不自己手动给ToolBar添加padding呢?
我们去掉ToolBar上的fitsSystemWindows属性,并设置一下ToolBar的padding,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12

protected void setStatusBarPaddingAndHeight(View toolBar) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (toolBar != null) {
int statusBarHeight = getSystemBarHeight(this);
toolBar.setPadding(toolBar.getPaddingLeft(), statusBarHeight, toolBar.getPaddingRight(),
toolBar.getPaddingBottom());
toolBar.getLayoutParams().height = statusBarHeight +
(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 45, getResources().getDisplayMetrics());
}
}
}

去掉ToolBar的fitsSystemWindows属性,并加上加上上面的代码,软键盘弹出时ToolBar正常了,但是输入框还是没有弹出来。

  • 解决方式1

刚才上面给ToolBar设置了fitsSystemWindows=true,结果ToolBar的内容被顶上去了,那我们能不能给输入框设置一个fitsSystemWindows=true属性呢?试一下就知道了!
试了之后你会发现,果然可以,但是输入框的高度变了,其实是输入框的padding增加了状态栏的高度。如果设计和产品能接受这种效果,那这也不失为一种解决方法。很显然,一般都不会接受这种效果的,就算设计和产品能接受,我们开发也不能接受!

那有没有更好的方法呢?到网上搜索发现下面一种解决方案。

  • 解决方式2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AndroidBug5497Workaround {

public static void assistActivity(View content) {
new AndroidBug5497Workaround(content);
}

private View mChildOfContent;
private int usableHeightPrevious;
private ViewGroup.LayoutParams frameLayoutParams;

private AndroidBug5497Workaround(View content) {
if (content != null) {
mChildOfContent = content;
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
});
frameLayoutParams = mChildOfContent.getLayoutParams();
}
}

private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
//如果两次高度不一致
//将计算的可视高度设置成视图的高度
frameLayoutParams.height = usableHeightNow;
mChildOfContent.requestLayout();//请求重新布局
usableHeightPrevious = usableHeightNow;
}
}

private int computeUsableHeight() {
//计算视图可视高度
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom);
}

}

添加上面的类,然后在Activity的onCreate方法中的setContentView后面加上如下代码:

1
AndroidBug5497Workaround.assistActivity(findViewById(android.R.id.content));

然后运行,输入框能够正常被顶上去,而且输入框的布局有没有受到影响。

该解决方案的原理是,给界面的根布局设置一个监听器,当界面大小有变化的时候,如键盘弹出的时候,重新设置一下根布局的高度,再调用requestLayout对界面进行重绘。

注:不知道这种解决方案会不会引起其他的问题,目前暂时没有发现,如果哪位知道有什么问题,请指点一下,谢谢!

华为EMUI3.1上的坑

将上面的沉浸式代码放在EMUI3.1系统的手机(如华为荣耀7)上面跑,你会发现,根本没有沉浸式效果,状态栏是透明的,显示的是桌面上的颜色,如下图:

经验证,原来是EMUI3.1系统的原因,很多App(如网易云音乐等)也是在EMUI3.0上有沉浸式的效果,到了EMUI3.1却没有效果了。在EMUI3.1没有沉浸式效果如果和4.4以前一样是黑的也就算了,这样透明的显示桌面颜色实在难看。
后来发现去掉下面这句代码,可以让其有沉浸式的效果。

1
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

效果如下:

不过它的状态栏不是全透明的,而是像某些4.4的系统一样是渐变的,不过总比原来的效果好。
这里我们加一个判断,判断如果不是EMUI3.1的系统,才调用clearFlags清除掉FLAG_TRANSLUCENT_STATUS。
具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// 因为EMUI3.1系统与这种沉浸式方案API有点冲突,会没有沉浸式效果。
// 所以这里加了判断,EMUI3.1系统不清除FLAG_TRANSLUCENT_STATUS
if (!isEMUI3_1()) {
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(Color.TRANSPARENT);
}
}
public static boolean isEMUI3_1() {
if ("EmotionUI_3.1".equals(getEmuiVersion())) {
return true;
}
return false;
}

private static String getEmuiVersion(){
Class<?> classType = null;
try {
classType = Class.forName("android.os.SystemProperties");
Method getMethod = classType.getDeclaredMethod("get", String.class);
return (String)getMethod.invoke(classType, "ro.build.version.emui");
} catch (ClassNotFoundException e) {
DebugUtil.exception(TAG,e);
} catch (NoSuchMethodException e) {
DebugUtil.exception(TAG,e);
} catch (IllegalAccessException e) {
DebugUtil.exception(TAG,e);
} catch (InvocationTargetException e) {
DebugUtil.exception(TAG,e);
} catch (Exception e){
DebugUtil.exception(TAG,e);
}
return "";
}

ActionMode上的坑

ActionMode是一种Context Menu,它悬浮在ToolBar活着ActionBar上面。现在已经基本上很少app在用ActionMode了,所以可能很多人可能没有用过,没用过的可以看看这篇文章http://blog.csdn.net/xyz_lmn/article/details/12754785。

公司项目中使用到了ActionMode(历史遗留代码),在实现沉浸式的效果中,发现ActionMode并不支持沉浸式。ActionMode弹出来的时候,状态栏会变成黑色的,效果如下:
ActionMode弹出前:

ActionMode弹出后:

遇到这个问题的时候,第一想法就是能不能和ToolBar一样给ActionMode设置一个值为状态栏高度的padding,然后将它顶到状态栏里面去。
在Stackoverflow上面搜了一种方法可以将ActionMode顶到状态栏上面去,给Activity加一个Flag即可,代码如下:

1
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);

看到能将ActionMode顶到状态栏中去时心里已经在开始偷着乐了,接下来只要给ActionMode设置一个padding即可。然而发现ActionMode根本没有提供在代码中设置高度和padding的API,只能在style中设置高度和padding。这样就有一个问题,因为Android手机碎片化严重,导致不同厂商的不同手机状态栏的高度不一致,所以使用这个方法会出现有的手机ActionMode弹出时比ToolBar高或者低,不过也还能接受。

如果仅仅这样也就算了,没想到又引起了另外一个问题。在使用上面的flag之后(flag不能乱加啊,血和泪的教训),虽然ActionMode顶到状态栏了,但是在某些(如华为)带虚拟按键的手机(虚拟按键对开发者来说也是一个大坑),虚拟按键会遮挡底部的布局。
只能放弃这种方案,尼玛,怎么这么多坑,让我哭会(泪崩)!

没办法,问题还是得去解决啊!继续寻找其它解决方案。。。

这时候想到了在Android5.0以上我们可以设置状态栏的颜色,那可不可以在ActionMode弹出来的时候,给状态栏设置一个与ToolBar颜色一致的颜色呢?尝试一下吧,在BaseActivity中重写startSupportActionMode方法,在里面给状态栏设置颜色,具体代码如下:

1
2
3
4
5
6
7
@Override
public ActionMode startSupportActionMode(ActionMode.Callback callback) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().setStatusBarColor(ContextCompat.getColor(getApplicationContext(), R.color.actionbar_background));
}
return super.startSupportActionMode(callback);
}

没想到,居然可以。不过只能兼容5.0以上的手机,4.4还是黑色。目前也只能这样了,后期项目中估计会将ActionMode干掉吧,到时候就OK了。如果大家又更好兼容ActionMode的方法请指点一下,谢谢!!!

参考文档

http://blog.csdn.net/lmj623565791/article/details/48649563
http://blog.csdn.net/brian512/article/details/52096445