真正完美的状态栏、导航栏“沉浸”,简单,高效!

1,622 阅读5分钟

当时刚入坑安卓,就是因为传统的状态栏、导航栏适配方法(获取它们的高度然后为布局设置 padding有巨大的 bug,所以我之前一直没有给我的软件安排“沉浸”状态栏与导航栏。多年以后的现在,我还是可以看到有源源不断的这样适配的文章被产出,适配极其繁琐,并且出来的效果还是有 bug。所以我写了这篇文章。

申明:我不是一名专业的安卓开发工程师,更没有经过专业的训练。如有错误,希望轻喷。

Screenshot_20240105_203917_com.androlua.jpg Screenshot_20240105_212423_com.androlua.jpg Screenshot_20240105_212423_com.androlua.jpg Screenshot_20240105_212431_com.androlua.jpg

华为横屏,无挖孔

Screenshot_2024-01-05-21-29-44-426_com.androlua.jpg

小米横屏,无挖孔,手势导航

Screenshot_2024-01-05-21-29-59-480_com.androlua.jpg

小米横屏,无挖孔,三键导航

使布局位于系统栏之下

结论

定义

首先我们要把状态栏和导航栏统称为“系统栏”,安卓官方也是这么叫的。

默认情况下,应用会放置在顶部状态栏的下方和底部导航栏的上方。状态栏和导航栏统称为系统栏。系统栏通常专门用于显示通知、传达设备状态以及进行设备导航。不过,您可以将应用配置为在这些区域显示内容。 来自 在窗口边衬区内布置应用  |  Android 开发者  |  Android Developers (google.cn)

系统栏.webp

系统栏.webp

小窗下的系统栏.webp

小窗下的系统栏.webp

如图,蓝色区域就是系统栏。

  • 系统栏可以出现在屏幕的任意一边。在不知道哪个安卓版本以上的导航栏也会显示在屏幕左边
  • 手机横屏并且为手势导航时,导航栏还是会在屏幕底部
  • 有时候,屏幕任何一边都没有系统栏。比如华为的小窗模式,虽然系统正在显示状态栏和导航栏,但它们没有覆盖到应用布局之上。
  • 有时候,系统两个栏都会显示,但只有一个栏在软件布局之上。比如小米的小窗。
  • 可能还有把状态栏和导航栏放一起的系统?
  • 还有更多情况...

如果按照传统方法,把这些情况挨个适配的话,代码将变得非常臃肿,而且稍不留神就出现了bug,并且以后如果设备制造商增加了什么功能,又得适配一遍。

所以,我们可以把状态栏和导航栏统一看作“系统栏”,然后向系统获取这个“系统栏”整体在应用布局的左、上、右、下分别嵌入的深度,然后再给让我们的布局避开这些区域,岂不美哉?

setOnApplyWindowInsetsListener

系统中还真有这么一个方法,是 view.setOnApplyWindowInsetsListener。但是由于兼容性,我们需要使用 ViewCompat.setOnApplyWindowInsetsListener

这个方法可以获取屏幕挖孔的深度、IME高度等,或者是无论是否打开IME,都只获取系统栏的高度。具体的用法可以看官方文档或者自行百度。这是我随便找的文章:Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow - 掘金 (juejin.cn)

网上也有不少的库提供了更便捷的用法,比如 Rikka 的 Insets,或者 Chris BanesInsetter,可以直接在布局文件中指定要避开的位置。

值得注意的点

  • 不要在第一次收到回调后将回调再次设置为空!华为设备会在不重载活动的情况下显示/隐藏导航栏!
  • 不要妄想获取状态栏、导航栏的高度!
  • 如果应用有适配挖孔屏,可以使用以下代码直接获取系统栏与挖孔区域的高度(挖孔的地方也会出现在屏幕四周),这样就避免又考虑一堆情况了。
int typeMask = WindowInsetsCompat.Type.systemBars() & WindowInsetsCompat.Type.displayCutout();
Insets insets = windowInsets.getInsets(typeMask);

系统栏颜色

其他的就是修改状态栏和导航栏的背景颜色和图标颜色了,比较简单。

部分系统的兼容性也不一样,安卓很晚才推出官方的修改图标颜色为暗色功能,不支持修改图标颜色的系统需要采用更深的背景颜色。(比如 #40000000

  • AOSP:
    • 状态栏图标变色:Android 6.0+(代码设置、主题定义 android:windowLightStatusBar
    • 导航栏图标变色:Android 8.0+(代码设置),Android 8.1+(主题定义android:windowLightNavigationBar
    • 导航栏、状态栏内容保护背景:Android 10+(代码设置、主题定义 android:enforceStatusBarContrastandroid:enforceNavigationBarContrast
    • 注意:其中 Android 8.0 在活动重载后不会使用新主题中定义的图标颜色,所以我们需要在 Activity.onCreate 中手动获取新主题中定义的图标颜色。(bug会在切换主题的时候出现)
  • 华为EMUI(包括鸿蒙4-)
    • 状态栏:我不知道
    • 导航栏:默认为灰色图标导航栏,所以无论版本如何都可以自由设置背景颜色(需要注意的是不能为 #40000000,否则无法看见图标了)。Android 8.0+ 也可以按照AOSP的方法指定是否为暗色导航栏。
  • 小米MIUI、阿里云等OS
    • 它们很早就有状态栏图标变色功能了。但适配方法我不知道。

对于 Android 8.0 的 bug,添加以下代码解决(这里我使用了内部方法MaterialAttributes.resolveBoolean(),你们千万别学我):

import com.google.android.material.resources.MaterialAttributes.resolveBoolean

// onCreate():
window.also {
    WindowCompat.getInsetsController(it, it.decorView).apply {
        isAppearanceLightNavigationBars =
            resolveBoolean(context, R.attr.windowLightNavigationBar, false)
        isAppearanceLightStatusBars =
            resolveBoolean(context, R.attr.windowLightStatusBar, false)
    }
}

注意:别使用 Android 4.4 的那个透明方法(主题定义 android:windowTranslucentStatus),这样做的后果是透明的背景略带阴影。