自 iPhone X 出了个刘海屏后,Android 各大厂商就先后跟进。由于 Android 碎片化严重,各大厂商各自为政,导致 Android 刘海屏的适配可谓痛苦,而网上的适配文章基本上只是简单的对官方文档做了一次搬运,对于业务线的同学来说,太不好使用了,因而我们需要做一次封装,解决各种兼容问题,让业务线最小程度感知刘海屏的存在。
QMUI 新版本就添加了 QMUINotchHelper
以及相关组件,这篇文章就是简要介绍 QMUI 的封装方案以及相关使用要点,可以从官网下载 apk,点击 helper -> QMUINotchHelper 体验效果
如果使用了 QMUI 的沉浸式方案,非全屏场景都会由沉浸式方案自动适配好,因此 QMUINotchHelper
只是针对全屏场景做的特殊兼容。
implementation "com.qmuiteam:qmui:1.1.7"
<meta-data
android:name="android.max_aspect"
android:value="2.34" />
<!-- huawei -->
<meta-data
android:name="android.notch_support"
android:value="true" />
<!-- xiaomi -->
<meta-data
android:name="notch.config"
android:value="portrait|landscape"/>
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
的实例呢 ?有两种方式:
onApplyWindowInsets
(或者使用 setOnApplyWindowInsetsListener
), 通过 windowInset.getDisplayCutout()
来获取;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 等手机添加了设置项而不提供接口(连文档都不说一下,只有踩坑后才知道...),让我们更列举下:
getRealScreenSize
也会随着全屏的取消与显示而有不同的值,这等价于getUsefulScreen
的效果。如果能够找到相应的 API, 那么这些方法也是可以逐步变得准确的,而目前而言,我也无话可说。
坑点 2:QMUI 的刘海屏并不能兼容到 Vivo、Oppo 等手机提供的所有设置项,更不能兼容到某些厂商白名单带来的不同效果
坑点 3:小米 8 在横屏状态下 WindowInsets 的左右值会出错,导致 fitSystemWindows 失效。此外,旋转屏幕,小米也不会重下发 windowInsets
绝大多数场景,我们需要的是 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。