当时刚入坑安卓,就是因为传统的状态栏、导航栏适配方法(获取它们的高度然后为布局设置 padding)有巨大的 bug,所以我之前一直没有给我的软件安排“沉浸”状态栏与导航栏。多年以后的现在,我还是可以看到有源源不断的这样适配的文章被产出,适配极其繁琐,并且出来的效果还是有 bug。所以我写了这篇文章。
申明:我不是一名专业的安卓开发工程师,更没有经过专业的训练。如有错误,希望轻喷。
华为横屏,无挖孔
小米横屏,无挖孔,手势导航
小米横屏,无挖孔,三键导航
使布局位于系统栏之下
结论
- 对于普通安卓开发者,看安卓官方的文档:在应用中全屏显示内容 | Android 开发者 | Android Developers (google.cn)
- 对于 AndroLua+ 用户,可以下载我的 demo,直接抄代码:jesse205.lanzouj.com/iZYIv132jxd…
- tip:因为 AndroLua+ 没有 AndroidX,所以需要使用原始的方法。为了代码的简洁,我们可以直接使用
insets.getSystemWindowInsetTop()等已废弃的方法(这几个方法也会将摄像头区域视为“系统栏”)。
- tip:因为 AndroLua+ 没有 AndroidX,所以需要使用原始的方法。为了代码的简洁,我们可以直接使用
- 不要妄想获取状态栏、导航栏的高度!
- 安卓官方管这玩意叫“无边框显示(edge to edge)”(我管他叫“边贴边”),沉浸指的是隐藏导航栏与状态栏。
定义
首先我们要把状态栏和导航栏统称为“系统栏”,安卓官方也是这么叫的。
默认情况下,应用会放置在顶部状态栏的下方和底部导航栏的上方。状态栏和导航栏统称为系统栏。系统栏通常专门用于显示通知、传达设备状态以及进行设备导航。不过,您可以将应用配置为在这些区域显示内容。 来自 在窗口边衬区内布置应用 | Android 开发者 | Android Developers (google.cn)
系统栏.webp
小窗下的系统栏.webp
如图,蓝色区域就是系统栏。
- 系统栏可以出现在屏幕的任意一边。在不知道哪个安卓版本以上的导航栏也会显示在屏幕左边。
- 手机横屏并且为手势导航时,导航栏还是会在屏幕底部。
- 有时候,屏幕任何一边都没有系统栏。比如华为的小窗模式,虽然系统正在显示状态栏和导航栏,但它们没有覆盖到应用布局之上。
- 有时候,系统两个栏都会显示,但只有一个栏在软件布局之上。比如小米的小窗。
- 可能还有把状态栏和导航栏放一起的系统?
- 还有更多情况...
如果按照传统方法,把这些情况挨个适配的话,代码将变得非常臃肿,而且稍不留神就出现了bug,并且以后如果设备制造商增加了什么功能,又得适配一遍。
所以,我们可以把状态栏和导航栏统一看作“系统栏”,然后向系统获取这个“系统栏”整体在应用布局的左、上、右、下分别嵌入的深度,然后再给让我们的布局避开这些区域,岂不美哉?
setOnApplyWindowInsetsListener
系统中还真有这么一个方法,是 view.setOnApplyWindowInsetsListener。但是由于兼容性,我们需要使用 ViewCompat.setOnApplyWindowInsetsListener。
这个方法可以获取屏幕挖孔的深度、IME高度等,或者是无论是否打开IME,都只获取系统栏的高度。具体的用法可以看官方文档或者自行百度。这是我随便找的文章:Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow - 掘金 (juejin.cn)
网上也有不少的库提供了更便捷的用法,比如 Rikka 的 Insets,或者 Chris Banes 的 Insetter,可以直接在布局文件中指定要避开的位置。
值得注意的点
- 不要在第一次收到回调后将回调再次设置为空!华为设备会在不重载活动的情况下显示/隐藏导航栏!
- 不要妄想获取状态栏、导航栏的高度!
- 如果应用有适配挖孔屏,可以使用以下代码直接获取系统栏与挖孔区域的高度(挖孔的地方也会出现在屏幕四周),这样就避免又考虑一堆情况了。
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:enforceStatusBarContrast与android:enforceNavigationBarContrast) - 注意:其中 Android 8.0 在活动重载后不会使用新主题中定义的图标颜色,所以我们需要在
Activity.onCreate中手动获取新主题中定义的图标颜色。(bug会在切换主题的时候出现)
- 状态栏图标变色:Android 6.0+(代码设置、主题定义
- 华为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),这样做的后果是透明的背景略带阴影。