从0到1优雅实现沉浸式状态栏

ImmersionStatusBar

1. 概述

现在市面上绝大多数APP都实现了沉浸式状态栏的效果,该效果可以极大地提升用户的使用体验。但是,APP默认的情况下只是设定一个固定的状态栏颜色,那么就需要我们通过代码去动态修改状态栏颜色的。那么,如何去实现呢?

首先来说,从Android 4.4开始才能实现沉浸式状态栏的,所以如果您的APP也支持Android 4.4以下版本,那还需要对Android 4.4以下版本做“不支持沉浸式状态栏”处理。

那么,从Android 4.4开始,大概可以分成三个阶段来实现沉浸式状态栏:

  • Android4.4(API 19) - Android 5.0(API 21):这个阶段的实现方式为:通过FLAG_TRANSLUCENT_STATUS设置状态栏为透明并且为全屏模式,然后通过添加一个与StatusBar一样大小的View,将View的背景设置为要设置的颜色,从而实现沉浸式。
  • Android 5.0(API 21) - Android 6.0(API 23): 从Android 5.0开始,加入了一个重要的属性android:statusBarColor和方法setStatusBarColor(),通过这个方法我们就可以轻松实现沉浸式状态栏。但是在Android 6.0以下版本官方不支持设置状态栏的文字和图标颜色,目前只有小米和魅族的ROM提供了支持。
  • Android 6.0(API 23)以上版本:其实Android 6.0以上的实现方式和Android 5.0+ 是一样的,区别是从Android 6.0开始,官方支持改变状态栏的文字和图标的颜色。

2. 实现方案

2.1 前期工作

为了实现沉浸式状态栏的效果,我们需要做一些前期工作,如下:

2.1.1 修改应用主题

为了更好地演示沉浸式状态栏的效果,我们修改应用的主题,使其不要显示Android默认的标题栏。

打开styles.xml文件,可以改成如下代码,并且将colorPrimary等颜色设置删掉:

1
2
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
</style>

2.1.2 获取状态栏高度

新建一个StatusBarUtils.java文件,在里面添加一个获取状态栏高度的方法,以后我们都会用的到这个方法。之后我们的核心代码也在这个工具类里实现。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StatusBarUtils {
public static int getHeight(Context context) {
int statusBarHeight = 0;
try {
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen",
"android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
} catch (Exception e) {
e.printStackTrace();
}
return statusBarHeight;
}
}

下面我们就按照从高版本到低版本的顺序来讲解如何更优雅地实现沉浸式状态栏。

2.2 Android 5.0+

2.2.1 设置状态栏颜色

StatusBarUtils类里添加如下方法,实现设置状态栏颜色的功能:

1
2
3
4
5
6
7
8
9
public static void setColor(@NonNull Window window, @ColorInt int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.setStatusBarColor(color);
}
}

修改状态栏颜色的功能其实就是对Window进行操作,而该Window可以是Activity或Dialog等持有的Window,所以我们就封装了一个传递Window的方法。

为了便于对Activity直接操作,可以再增加一个如下方法:

1
2
3
4
5
public static void setColor(Context context, @ColorInt int color) {
if (context instanceof Activity) {
setColor(((Activity) context).getWindow(), color);
}
}

下面测试一下,新建一个Activity,将其布局背景设置为某个颜色,例如主题色,布局代码如下:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/colorPrimaryDark">

</LinearLayout>

在没设置状态栏颜色时,展示效果如下:

接下来在Activity的onCreate()方法里调用设置状态栏颜色的方法,代码如下:

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color);
StatusBarUtils.setColor(this, getResources().getColor(R.color.colorPrimaryDark));
}

展示效果如下:

这样状态栏和内容的颜色就可以一样啦!

2.2.2 设置状态栏文字颜色

下面我们把背景和状态栏颜色改成纯绿色(#00FF00),看看效果:

发现颜色好亮啊,状态栏里的文字都看不清了。在这种情况下,我们是可以将状态栏文字的颜色改成深色的,官方也仅支持设置状态栏文字和图标的深色模式和浅色模式,但是官方仅在Android 6.0以上版本提供支持。设置代码如下:

1
2
3
4
5
6
7
8
9
10
11
private static void setTextDark(Window window, boolean isDark) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
View decorView = window.getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
if (isDark) {
decorView.setSystemUiVisibility(systemUiVisibility | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
decorView.setSystemUiVisibility(systemUiVisibility & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
}

同样再增加一个对Activity的支持:

1
2
3
4
5
public static void setTextDark(Context context, boolean isDark) {
if (context instanceof Activity) {
setTextDark(((Activity) context).getWindow(), isDark);
}
}

为了能够根据状态栏背景颜色的深浅而自动设置文字的颜色,我们再新增一个判断颜色深浅的方法:

1
2
3
public static boolean isDarkColor(@ColorInt int color) {
return ColorUtils.calculateLuminance(color) < 0.5;
}

然后在setColor()方法里新增一行设置状态栏文字颜色的代码,如下:

1
2
3
4
5
6
7
8
9
10
public static void setColor(@NonNull Window window, @ColorInt int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.setStatusBarColor(color);
setTextDark(window, !isDarkColor(color));
}
}

运行一下,就可以看到效果了:

上面是Android 6.0以上版本的实现,那么对于Android 6.0以下的手机怎么办呢?目前Android 5.0-6.0的手机只有小米MIUI和魅族Flyme系统提供了支持。小米MIUI的设置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void setMIUIDark(Window window, boolean isDark) {
try {
Class<? extends Window> clazz = window.getClass();
int darkModeFlag;
Class<?> layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
darkModeFlag = field.getInt(layoutParams);
Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
extraFlagField.invoke(window, isDark ? darkModeFlag : 0, darkModeFlag);
} catch (Exception e) {
e.printStackTrace();
}
}

魅族Flyme的设置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void setFlymeDark(Window window, boolean isDark) {
if (window != null) {
try {
WindowManager.LayoutParams lp = window.getAttributes();
Field darkFlag = WindowManager.LayoutParams.class
.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
Field meizuFlags = WindowManager.LayoutParams.class
.getDeclaredField("meizuFlags");
darkFlag.setAccessible(true);
meizuFlags.setAccessible(true);
int bit = darkFlag.getInt(null);
int value = meizuFlags.getInt(lp);
if (isDark) {
value |= bit;
} else {
value &= ~bit;
}
meizuFlags.setInt(lp, value);
window.setAttributes(lp);
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后在setTextDark()方法中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void setTextDark(Window window, boolean isDark) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
View decorView = window.getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
if (isDark) {
decorView.setSystemUiVisibility(systemUiVisibility | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
decorView.setSystemUiVisibility(systemUiVisibility & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
switch (OSUtils.getRomType()) {
case MIUI:
setMIUIDark(window, isDark);
break;
case Flyme:
setFlymeDark(window, isDark);
break;
default:
}
}
}

Android 6.0以下运行效果同上,我就不给大家截图了。

2.2.3 设置状态栏透明

当我们APP的背景是一张图片时,未设置沉浸式状态栏的效果如下:

这时一般我们需要将图片顶到状态栏里,也就是整个内容布局顶到状态栏里,并设置状态栏的颜色透明,才能实现沉浸式状态栏的效果。

那么,在我们的StatusBarUtils类里添加如下代码:

1
2
3
4
5
6
7
8
9
public static void setTransparent(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
}
}

同样针对Activity,增加如下方法:

1
2
3
4
5
public static void setTransparent(Context context) {
if (context instanceof Activity) {
setTransparent(((Activity) context).getWindow());
}
}

最后在Activity的onCreate()方法里设置一下:

1
2
3
4
5
6
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_transparent);
StatusBarUtils.setTransparent(this);
}

运行,显示效果如下:

针对这种情况,大家在实现布局时不要忘了把状态栏的高度也考虑进去。

以上就是Android 5.0以上沉浸式状态栏的实现了。

2.3 Android 4.4+

下面针对Android 4.4-5.0的手机进行实现。实现原理是将内容布局设为全屏,然后在布局的顶部添加一个和状态栏一样高度的View,将该View的背景设置成我们想要的颜色。当需要将状态栏设置纯颜色时,为了和Android 5.0以上版本保持一致,我们对内容布局的上边设置一个padding,大小为状态栏的高度。

为了能复用这个View,我们新增一个自定义的ID,在values文件夹下新建ids.xml文件,新增代码如下:

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="fake_status_bar_view" type="id"/>
</resources>

然后在StatusBarUtils类里添加如下代码:

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
private static final int FAKE_STATUS_BAR_VIEW_ID = R.id.fake_status_bar_view;

@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static void setColor(@NonNull Window window, @ColorInt int color,
boolean isTransparent) {
Context context = window.getContext();
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
ViewGroup decorView = (ViewGroup) window.getDecorView();
View contentView = decorView.findViewById(android.R.id.content);
if (contentView != null) {
contentView.setPadding(0, isTransparent ? 0 : getHeight(context), 0, 0);
}
View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
if (fakeStatusBarView != null) {
fakeStatusBarView.setBackgroundColor(color);
if (fakeStatusBarView.getVisibility() == View.GONE) {
fakeStatusBarView.setVisibility(View.VISIBLE);
}
} else {
// 绘制一个和状态栏一样高的矩形
View statusBarView = new View(context);
FrameLayout.LayoutParams layoutParams =
new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
getHeight(context));
statusBarView.setLayoutParams(layoutParams);
statusBarView.setBackgroundColor(color);
statusBarView.setId(FAKE_STATUS_BAR_VIEW_ID);
decorView.addView(statusBarView);
}
}

2.3.1 设置状态栏颜色

在设置纯颜色时,我们还需要将该颜色与黑色进行1:1的混合。为什么要这么设置呢?因为状态栏的文字和图标颜色默认是白色的,并且在Android 5.0以下是不能修改的,所以如果修改成较浅的颜色,就会导致状态栏文字看不清的现象,因此做一个比较暗的浮层效果更好一些。

那么将setColor()方法改成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static void setColor(@NonNull Window window, @ColorInt int color) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.setStatusBarColor(color);
setTextDark(window, !isDarkColor(color));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setColor(window, ColorUtils.blendARGB(Color.TRANSPARENT, color, 0.5f), false);
}
}

展示效果如下:

2.3.2 设置状态栏透明

在设置状态栏透明时,为了也能清楚地看清状态栏的文字,我们直接设置状态栏的颜色为50%透明度的黑色。

于是,修改setTransparent()方法如下:

1
2
3
4
5
6
7
8
9
10
11
public static void setTransparent(@NonNull Window window) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
setColor(window, 0x80000000, true);
}
}

展示效果如下:

这样,Android 4.4以上的手机就适配完毕了。

3. 特殊场景

现在来针对一些特殊场景进行适配,比如启动页、刘海屏和弹窗等等。

3.1 启动页

对于启动页,一般都会把状态栏收上去,这需要适配刘海屏,否则刘海区域会显示黑的一片。其实Android P以上提供了适配刘海屏的方法,在启动页Activity添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
StatusBarUtils.setTransparent(this);
// 适配刘海屏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
}

但是一些手机厂商的刘海屏手机系统版本是低于Android P的,不过也都提供了适配的方法。适配方式是在AndroidManifest.xml文件里的application标签下添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 允许绘制到小米刘海屏机型的刘海区域 -->
<meta-data
android:name="notch.config"
android:value="portrait" />
<!-- 允许绘制到华为刘海屏机型的刘海区域 -->
<meta-data
android:name="android.notch_support"
android:value="true" />
<!-- 允许绘制到oppo、vivo刘海屏机型的刘海区域 -->
<meta-data
android:name="android.max_aspect"
android:value="2.2" />

另外,对于Android 5.0以下的手机,适配完刘海屏后会在顶部多一块黑色半透明的View,那我们将其改成全透明的,修改onCreate()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
StatusBarUtils.setTransparent(this);
// 适配刘海屏
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
// 适配Android 4.4
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
StatusBarUtils.setColor(getWindow(), Color.TRANSPARENT, true);
}
}

这样,启动页的效果就适配好了,效果如下:

3.2 弹窗

对于弹窗,只有全屏弹窗才能修改状态栏文字的颜色。详见Android非全屏的Window无法设置SYSTEM_UI_FLAG_LIGHT_STATUS_BAR问题分析

4.参考

坚持原创技术分享,您的支持将鼓励我继续创作!