前言
本文总结 Android 实现沉浸式全屏的实现方式。
实现沉浸式全屏
在一些需要全屏显示的场景下,比如玩游戏、看横屏视频的时候,内容全屏,占满窗口的体验会让用户更加沉浸到对内容的消费中,带来好的用户体验。
沉浸式显示具体来说就是如状态栏和导航栏部分的显示效果调整。当然,这里对于不同的产品形态会有不同的选择,状态栏文本的颜色、状态栏本身的背景色、导航栏的背景色以及是否显示,通过这些组合可以呈现出不同的用户体验。下面就从这两个组件的使用出发,看看实现沉浸式状态栏的方法。
状态栏
状态栏背景色
关于状态栏,首先是状态栏背景色, 这个根据需要设置就好了,一般情况下设置为透明比较好适配。
window.statusBarColor = Color.TRANSPARENT
状态栏文字颜色
关于状态栏、导航栏的其他操作,我们可以使用系统的 WindowInsetsControllerCompat
这个类,从名字Compat 就可以看到,这是一个兼容的类。关于沉浸式状态栏的实现,由于 Android 在国内变成了「安卓」,因此早期关于状态各种属性的适配可以说是群魔乱舞,各式各样的 StatusBarUtils 大行其道。现在好了,Android 官方终于一统天下,亲自下场来搞了,这下关于沉浸式的实现就比较简单了。
WindowInsetsControllerCompat
的使用也很简单,创建一个他的实例即可。
val controller = WindowInsetsControllerCompat(window, window.decorView)
后面的一切使用这个实例就可以了,API 很简单,命名一目了然。
比如更改状态颜色这个功能。关于状态栏文字的颜色,Android 官方只允许设置黑色或者白色
controller.isAppearanceLightStatusBars = true // 黑色状态栏
// or
controller.isAppearanceLightStatusBars = false // 白色状态栏
注意、注意、注意 ,这里的注释没有写错,这个方法就是这么奇怪,自己一开始使用也是被绕晕了。但就是这样。还有一点需要注意的是,Android 6.0 也就是 Android SDK 23 开始,才可以使用这个 feature 。
状态栏显示与隐藏
controller.hide(WindowInsetsCompat.Type.statusBars()) // 状态栏隐藏
// or
controller.show(WindowInsetsCompat.Type.statusBars()) // 状态栏显示
这个就很简单了。
导航栏
说完了状态栏,在来看导航栏。相比状态栏,导航栏上不会有文字,一般情况下就是一条底部的横线。因此,我们只需要关心导航的背景色和可见性即可。
导航栏背景色
window.navigationBarColor = Color.TRANSPARENT
导航栏横线的颜色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
从 Android P (28) 也就是 Android 9.0 开始,我们甚至可以设置导航栏横线的颜色了。
潜在的坑
这里再补充一个最近遇到的关于设置状态栏和导航栏颜色的坑。如果当前 Activity 的 theme 属性中包含如下内容
<style name="BugTheme" parent="AppTheme">
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowTranslucentStatus">true</item>
</style>
这里的坑就是由这个 Translucent 导致的。Translucent 的意思是半透明,Transparent 的意思才是透明。这两个单词从拼写到发音都有些相似,理论上说二者的效果也是类似,无非就是透明度的值不一样而已。但就是这细微的差别,容易导致问题。一旦给 Activity 的 theme 设置了如上的熟悉,那么后续通过 window.statusBarColor
和 window.navigationBarColor
设置颜色就不在生效了。
导航栏显示与隐藏
导航栏显示与隐藏的方法,和状态栏显示隐藏的方法非常相似,改变一下参数即可。
controller.hide(WindowInsetsCompat.Type.navigationBars()) // 导航栏隐藏
// or
controller.show(WindowInsetsCompat.Type.navigationBars()) // 导航栏显示
沉浸式
WindowCompat.setDecorFitsSystemWindows(window, false) // 打开沉浸式
// or
WindowCompat.setDecorFitsSystemWindows(window, true) // 关闭沉浸式
沉浸式开 | 沉浸式关闭 |
---|---|
可以看到,使用 WindowInsetsControllerCompat
,沉浸式就是这么简单。
适配全面屏
从上面沉浸式的图,可以看到其实还是有点问题,就是横屏之后,屏幕左边并没有完全铺开,而是有一段黑边,看着非常难受了。这其实是关于异形屏的适配问题。
其实这段黑边就是刘海屏的区域,Android 官方叫做 DisplayCutout area
val params = window.attributes
params.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
window.attributes = params
大于等于 Android 9.0 (SDK 28 P)的设备都支持。关于 layoutInDisplayCutoutMode
参数有三种类型。
关于这三个参数,还要考虑当前屏幕是横屏还是竖屏。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
这个是默认参数。
竖屏状态
如果没有设置全屏显示的属性,那么内容是延伸到 DisplayCutout area
的。这种情况下,如果进行全屏和非全屏的切换操作,会发现内容在整体上下跳动。比如在这种情况下,调用 controller.hide(WindowInsetsCompat.Type.statusBars())
进行状态栏的操作,就会发现整个内容在上下跳动。在上面的动图里很明显了。
横屏状态
横屏状态下,顶部的状态栏就变成左边或者右边(这里看屏幕是怎么旋转的,可能是旋转了 -90 度,也可能是 270 度)的黑边了。内容不会延伸到左右两边的 DisplayCutout area
里。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
这种情况下,内容是默认延伸到 DisplayCutout area
的。因此,全屏和非全屏操作的时候,就不会有内容上下跳动或者屏幕上说的问题。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
这种情况,内容永远不会延伸到 DisplayCutout area
里面。
关于 LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 和 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 的区别,我们用两张图比较一下就大概明白了。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES |
---|---|
可以看到,LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 模式下,整个内容都撑开到刘海的区域了。屏占比更高了,看着也更舒服了。
总结
使用官方提供的 WindowInsetsControllerCompat
的系列 API,操作状态栏及导航栏,以及沉浸式的实现相对来说比较简单了,底层处理了各个版本之间的兼容性。