小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

QMUI 刘海屏适配方案

自 iPhone X 出了个刘海屏后,Android 各大厂商就先后跟进。由于 Android 碎片化严重,各大厂商各自为政,导致 Android 刘海屏的适配可谓痛苦,而网上的适配文章基本上只是简单的对官方文档做了一次搬运,对于业务线的同学来说,太不好使用了,因而我们需要做一次封装,解决各种兼容问题,让业务线最小程度感知刘海屏的存在。

QMUI 新版本就添加了 QMUINotchHelper 以及相关组件,这篇文章就是简要介绍 QMUI 的封装方案以及相关使用要点,可以从官网下载 apk,点击 helper -> QMUINotchHelper 体验效果

如果使用了 QMUI 的沉浸式方案,非全屏场景都会由沉浸式方案自动适配好,因此 QMUINotchHelper 只是针对全屏场景做的特殊兼容。

引入QMUI

implementation "com.qmuiteam:qmui:1.1.7"  

兼容平台

  • 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" />

<!--  xiaomi -->  
<meta-data  
    android:name="notch.config"
    android:value="portrait|landscape"/>

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的实例呢 ?有两种方式:

  1. 在 View 中重写 onApplyWindowInsets(或者使用 setOnApplyWindowInsetsListener), 通过 windowInset.getDisplayCutout() 来获取;
  2. 当 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 上,获取屏幕宽高信息的就有三套了: getScreenSizegetUsefulScreengetRealScreenSize。 (使用者更加蛋疼了,可能连 getScreenSizegetRealScreenSize 的区别都不知道...)

提供了这两个方法,但是其实并不好用,因为并不是特别准确,不准确的原因就是 Vivo、Oppo 等手机添加了设置项而不提供接口(连文档都不说一下,只有踩坑后才知道...),让我们更列举下:

  • Vivo 设置-系统导航-导航手势样式-显示手势操作区域 打开的情况下,应该减去手势操作区域的值,但无判断 API。
  • Vivo 设置-显示与亮度-第三方应用显示比例 选为安全区域显示时,整个 window 会移动,应该减去移动区域的值,但无判断 API。
  • Oppo 设置-显示与亮度-应用全屏显示-凹形区域显示控制 关闭是,整个 window 会移动,应该减去移动区域,但无判断 API。
  • Essential Phone 升级到 Android 8 后,在开发者选项中也提供了设置项,但也没有相关 API。 此外 Essential Phone 的 getRealScreenSize 也会随着全屏的取消与显示而有不同的值,这等价于getUsefulScreen 的效果。

如果能够找到相应的 API, 那么这些方法也是可以逐步变得准确的,而目前而言,我也无话可说。

坑点2:QMUI 的刘海屏并不能兼容到 Vivo、Oppo 等手机提供的所有设置项,更不能兼容到某些厂商白名单带来的不同效果

坑点3:小米 8在横屏状态下 WindowInsets 的左右值会出错,导致 fitSystemWindows 失效。此外,旋转屏幕,小米也不会重下发 windowInsets

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();  

如果是第三方厂商实现,需要在 onAttachedToWindowonConfigurationChanged 处理,处理方式也很简单,通过 padding 消耗掉不安全区域:

setPadding(  
    QMUINotchHelper.getSafeInsetLeft(this),
    QMUINotchHelper.getSafeInsetTop(this),
    QMUINotchHelper.getSafeInsetRight(this),
    QMUINotchHelper.getSafeInsetBottom(this)
);

基本上就是这么多。当然,各大厂商的 API 也是朝令夕改, 也不知道升级到 Android P 后会不会遵循官方的方案,因此刘海屏的适配也只能走一步看一步。测试机型也很有限,如果发现不完善的地方或者未适配的机型,欢迎提 issue。

←微信← →支付宝 →