QMUI 刘海屏适配方案
自 iPhone x 出了个刘海屏后,Android 各大厂商就先后跟进。由于 Android 碎片化严重,各大厂商各自为政,导致 Android 刘海屏的适配可谓痛苦,而网上的适配文章基本上只是简单的对官方文档做了一次搬运,对于业务线的同学来说,太不好使用了,因而我们需要做一次封装,解决各种兼容问题,让业务线最小程度感知挖孔屏的存在。
QMUI 新版本就添加了 QMUINotchHelper
以及相关组件,这篇文章就是简要介绍 QMUI 的封装方案以及相关使用要点。
如果使用了 QMUI 的沉浸式方案,刘海屏就只有在有全屏场景的 App 需要做特殊兼容
引入QMUI
implementation "com.qmuiteam:qmui:1.1.6"
兼容平台
- Android P+(官方)
- 小米
- 华为
- Vivo
- Oppo
- Essential Phone
AndroidManifest 设置
<meta-data android:name="android.max_aspect" android:value="2.34" /> <!-- huawei --> <meta-data android:name="android.notch_support" android:value="true" />
QMUINotchHelper
QMUI 的接口参考 Android P 官方接口,提供了如下主要几个接口:
// 是否有刘海屏 QMUINotchHelper.hasNotch(Activity | View) // 左边的安全距离 QMUINotchHelper.getSafeInsetLeft(Activity | View) // 上边的安全距离 QMUINotchHelper.getSafeInsetTop(Activity | View) // 右边的安全距离 QMUINotchHelper.getSafeInsetRight(Activity | View) // 下边的安全距离 QMUINotchHelper.getSafeInsetBottom(Activity | View)
或许有人觉得奇怪:为何传参都是 Activity
或者 View
, 而不是 Context
?这我们需要知道 Android P 是如何去适配刘海屏的: Android P 提供了 DisplayCutout
类, 那么如何获取 DisplayCutout
的实例呢 ?有两种方式:
- 在 View 中重写
onApplyWindowInsets
(或者使用setOnApplyWindowInsetsListener
), 通过windowInset.getDisplayCutout()
来获取; - 当 View 已经 attach 到 window 上时, 通过
view.getRootWindowInsets().getDisplayCutout()
。
第一种方式获取到的值在全屏和非全屏下是不一样的。非全屏下,得到的值为 null, 如果我们的 App 需要动态切换全屏与非全屏,我们获取的可布局区域不一样,这很容易造成界面跳动,因此不可取。 第二种方案, 很少有人或文档提及,但是是非常准确的,因此 QMUI 里面基本上都是依靠方式2来完成 Android P 的适配的。当然,如果 view 没有 attach 到 window 上, 那么就得不到 rootWindowInsets
信息, 因此这是一个坑点:
坑点1:通过 QMUINotchHelper 获取刘海屏信息并传参为 View 时,View 必须是已经 attach 到 window 上了的。
获取屏幕可用宽高信息
除了 QMUINotchHelper
外, QMUIDisplayHelper
添加了两个重要方法:
// 获取屏幕可用宽度 QMUIDisplayHelper.getUsefulScreenWidth(Activity | View) // 获取屏幕可用高度 QMUIDisplayHelper.getUsefulScreenHeight(Activity | View)
为何需要这几个方法?因为华为、Vivo、Oppo、小米这国内四巨头在设置里都有诸如是否使用刘海区域的设置项。如果不使用,那么就会把整个 window 进行偏移,所以 getRealScreenSize
并不能代表可以使用的区域,所以在 QMUI 里增加这两个方法,帮助开发者处理掉不能使用的区域。 因此,在 QMUI 上,获取屏幕宽高信息的就有三套了: getScreenSize
、getUsefulScreen
、getRealScreenSize
。 (使用者更加蛋疼了,可能连 getScreenSize
和 getRealScreenSize
的区别都不知道...)
提供了这两个方法,但是其实并不好用,因为并不是特别准确,不准确的原因就是 Vivo、Oppo 等手机添加了设置项而不提供接口(连文档都不说一下,只有踩坑后才知道...),让我们更列举下:
- Vivo 设置-系统导航-导航手势样式-显示手势操作区域 打开的情况下,应该减去手势操作区域的值,但无判断 API。
- Vivo 设置-显示与亮度-第三方应用显示比例 选为安全区域显示时,整个 window 会移动,应该减去移动区域的值,但无判断 API。
- Oppo 设置-显示与亮度-应用全屏显示-凹形区域显示控制 关闭是,整个 window 会移动,应该减去移动区域,但无判断 API。
- Essential Phone 升级到 Android 8 后,在开发者选项中也提供了设置项,但也没有相关 API。 此外 Essential Phone 的
getRealScreenSize
也会随着全屏的取消与显示而有不同的值,这等价于getUsefulScreen
的效果。
如果能够找到相应的 API, 那么这些方法也是可以逐步变得准确的,而目前而言,我也无话可说。
坑点2:QMUI 的刘海屏并不能兼容到 Vivo、Oppo 等手机提供的所有设置项,更不能兼容到某些厂商白名单带来的不同效果
QMUINotchConsumeLayout
绝大多数场景,我们需要的是View 最外层容器消耗掉 Notch 带来的不安全区域,所以我提供了一个简单的容器类:QMUINotchConsumeLayout
, 其需要配合 QMUIWindowInsetLayout
等实现了 IWindowInsetLayout
的容器类来使用, 例如 QMUIDemo 给的使用案例:
<com.qmuiteam.qmui.widget.QMUIWindowInsetLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/qmui_config_color_white"> <com.qmuiteam.qmui.widget.QMUINotchConsumeLayout android:id="@+id/not_safe_bg" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 具体内容 --> </com.qmuiteam.qmui.widget.QMUINotchConsumeLayout> </com.qmuiteam.qmui.widget.QMUIWindowInsetLayout》
如果 QMUINotchConsumeLayout
无法满足需求, 可以参考QMUINotchConsumeLayout
在 View 层级里灵活处理:
首先,需要实现 INotchInsetConsumer
来接收 Android P+ 上Notch 信息 的派发,这个接口提供了一个方法:
// 返回 true 时,停止向子 View 派发 Notch 信息 boolean notifyInsetMaybeChanged();
如果是第三方厂商实现,需要在 onAttachedToWindow
和 onConfigurationChanged
处理,处理方式也很简单,通过 padding 消耗掉不安全区域:
setPadding( QMUINotchHelper.getSafeInsetLeft(this), QMUINotchHelper.getSafeInsetTop(this), QMUINotchHelper.getSafeInsetRight(this), QMUINotchHelper.getSafeInsetBottom(this) );
基本上就是这么多。当然,各大厂商的 API 也是朝令夕改, 也不知道升级到 Android P 后会不会遵循官方的方案,因此刘海屏的适配也只能走一步看一步。测试机型也很有限,如果发现不完善的地方或者未适配的机型,欢迎提 issue。