号外!号外!全网第一手Android P刘海屏适配大揭秘,唯一Kotlin算法

code小生,一个专注于 Android 领域的技术分享平台

作者:keyu88888
地址:https://www.jianshu.com/p/40630ecb06fb
声明:本文是 keyu88888 原创,转发等请联系原作者授权。

1. 背景

Apple 一直在引领设计的潮流,自从 iPhone X 发布之后,刘海屏就一直存在争议。不过不管你怎样,Android也要跻入“刘海屏“的行列,Android P 预览版增加了很多亮点新特性,其中最接地气、最直观的改变当属适配了类似于华为 P20 的顶部凹槽屏幕设计这一项,也从系统级支持顶部凹槽屏幕设计。

很多厂商也在逐渐推出刘海屏设计的手机,在国内比较常见的就是OPPO R15和华为P20。

各厂家的刘海屏

1.1.介绍
刘海屏的外观,我想大家应该都有概念,不过不同厂商刘海屏的实现方式也有所不太,这一点需要先有个概念。

就现在市场上的情况来说,会区分成两类,一类是标准的 Android P Api,另外一类就是厂商在 Android P 以下的系统,做的特殊适配。

例如:华为 P20 就是采用的 Android P 标准 Api 的方式,而 OPPO R15 就不一样了,它有自己的适配 Api。

1.2.需要适配的情况
Android P版本提供了统一的刘海屏方案和三方适配刘海屏方案:

  • 对于有状态栏的页面,不会受到刘海屏特性的影响

  • 全屏显示的页面,系统刘海屏方案会对应用界面做下移处理,避开刘海区显示

  • 已经适配Android P应用的全屏页面可以通过谷歌提供的适配方案使用刘海区,真正做到全屏显示。

2. 搭建环境

在手边没有对应系统的设备的时候,模拟器是一条不错的路,最近 Google 也发布了 Android P 的模拟器,还有一个办法就是找一些支持真机云测的平台,例如华为的云测平台,也是一个解决方案,不过没有本地模拟机这么便捷。

2.1.华为终端开放实验室

华为终端开放实验室

2.2.本地模拟器
选择Android P的模拟器,有需要自己更新SDK,下载更新就好。

下载Android P的Rom

刘海的凹槽区域,大部分是为了给摄像头或者其他传感器留出区域。而在没有刘海的设备或者模拟器上,可以通过开发者选项里的 “Simulate a display with a cutout”,开启刘海屏的支持。

模拟刘海屏

Android P模拟器自带四种刘海屏的模式,分别为:“None”、“Narrow display cutout”、“Tall display cutout”和“Wide display cutout”。如下图所示:

NoneNarrow display cutoutTall display cutoutWide display cutout

3. 兼容性影响

上面也讲清楚了,刘海屏的切割区域,都存在于状态栏上,所以在有状态栏的页面上,是无需我们特殊处理的,系统会帮我们处理好。

而对于全屏的页面,就需要单独的处理了。我这里,简单做了一个全屏页面,每个横条都是等宽的这样能看到布局上的差异。

关闭刘海屏开启刘海屏但不支持适配刘海屏

一个全屏的页面,当没有支持刘海屏又碰到了刘海屏,会导致 UI 下沉,如果这不是一个列表的布局,底部的控件就会被遮挡。

还有一些被刘海遮挡区域的效果,其实主要是依赖 UI 设计师来规避了,不要在可能出现刘海切割的地方,设计可操作的区域,影响用户操作。

4. 官方刘海屏适配

说那么多,最终我们还是需要用技术的方式来适配刘海屏。Android P 的刘海屏,是有标准的Api来进行适配,而对于一些厂商自己的刘海屏设备,例如:OPPO R15,就需要遵循它的开发文档进行单独适配。

Android P 为最新的刘海屏,提供了专门的Api来支持:DisplayCutout。

4.1.开启刘海屏
在非刘海屏P版本手机可以开启模拟刘海屏调试的功能

  • 在开发人员选项屏幕中,向下滚动到绘图部分,然后点击“模拟具有凹口的显示屏”设置项

  • 选择刘海尺寸信息
      如下图所示:

开启刘海屏模拟刘海屏

4.2.适配刘海屏
在刘海屏调试打开之后,浏览应用的所有页面,测试所有遮挡问题,或者是下移导致的问题,对有问题的页面进行布局适配。适配方案如下:

Google 提供的适配方案,可以设置是否在全屏模式下,使用刘海屏的区域。

// 谷歌官方提供的默认适配刘海屏
val attrib = window.attributes
attrib.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

新的布局属性 layoutInDisplayCutoutMode 包含三种可选的模式,分别为:

// 窗口声明使用刘海区域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 1;
// 默认情况下,全屏窗口不会使用到刘海区域,非全屏窗口可正常使用刘海区域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;
// 声明不使用刘海区域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;

4.3.刘海屏的高度
在全屏模式下,我们的应用页面背景充满整个屏幕显示,控件和文字等关键信息布局在状态栏以外的区域,以保证关键信息不会出现遮挡(谷歌要求:凹槽高度和刘海高度要保持一致)。我们需要有办法获取到刘海屏凹槽的高度,才可以做到设计和布局的时候,留出安全距离。

4.3.1. 获取刘海尺寸信息接口
Android P已经预留出了标准的测量刘海屏凹槽的Api:DisplayCutout。

DisplayCutout

刘海屏的凹槽,就在屏幕的中间,所以只有getSafeInsetTop()方法返回的结果,是我们需要的,而其他的getSafeInsetXXX()方法,直接返回的是0。代码如下所示:

btn_always.postDelayed(Runnable {

   val displayCutout = btn_always.rootWindowInsets.displayCutout
   if (null == displayCutout)
{
       Log.e(TAG, "displayCutout is empty")
       return@Runnable
   }
   Log.i(TAG, "SafeInsetBottom:" + displayCutout.safeInsetBottom);
   Log.i(TAG, "SafeInsetLeft:" + displayCutout.safeInsetLeft);
   Log.i(TAG, "SafeInsetRight:" + displayCutout.safeInsetRight);
   Log.i(TAG, "SafeInsetTop:" + displayCutout.safeInsetTop);

}, 100)

输出结果为:

SafeInsetBottom:0
SafeInsetLeft:0
SafeInsetRight:0
SafeInsetTop:84

4.3.2. 获取系统状态栏高度接口
获取刘海屏的高度之后,我们还要获取系统状态栏的高度,代码如下:

fun getStatusBarHeight(context: Context): Int {

   var result = 0

   val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
   if (0 < resourceId) {
       result = context.resources.getDimensionPixelOffset(resourceId)
   }

   return result
}

5. 其他厂商刘海屏适配

像华为、Oppo和Vivo这样的厂商,实现刘海屏的方式,也并不是按照 Android P的标准做的,它完全是自己修改了刘海屏的实现方式。不过他们是会提供完备的适配文档,这就需要我们直接阅读他们提供的开发文档来进行适配。各个厂商的刘海屏适配参考如下:

厂商    介绍
华为    https://mini.eastday.com/bdmip/180411011257629.html
Oppo    https://open.oppomobile.com/wiki/doc#id=10159
Vivo    https://dev.vivo.com.cn/doc/document/info?id=103

5.1.华为
华为提供了刘海屏Api,可以通过反射的方式调用。

5.1.1. 判断是否刘海屏接口
代码如下:

/**
* 判断是否是华为刘海屏
* @param context 上下文对象
* @return true:是刘海屏;false:非刘海屏
*/

fun hasNotchInScreen(context: Context): Boolean {
   var ret = false
   try {
       val cl = context.getClassLoader()
       val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
       val method = HwNotchSizeUtil.getMethod("hasNotchInScreen")
       ret = method.invoke(HwNotchSizeUtil) as Boolean

   } catch (e: ClassNotFoundException) {
       Log.e(TAG, "hasNotchInScreen ClassNotFoundException")
   } catch (e: NoSuchMethodException) {
       Log.e(TAG, "hasNotchInScreen NoSuchMethodException")
   } catch (e: Exception) {
       Log.e(TAG, "hasNotchInScreen Exception")
   } finally {
       return ret
   }
}

5.1.2. 获取刘海尺寸信息接口
代码如下:

/**
* 获取华为刘海的高宽
* @param context 上下文对象
* @return  [0]值为刘海宽度int;[1]值为刘海高度
*/

fun getNotchSize(context: Context): IntArray {
   var ret = intArrayOf(0, 0)
   try {
       val cl = context.classLoader
       val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
       val method = HwNotchSizeUtil.getMethod("getNotchSize")
       ret = method.invoke(HwNotchSizeUtil) as IntArray
   } catch (e: ClassNotFoundException) {
       Log.e(TAG, "getNotchSize ClassNotFoundException")
   } catch (e: NoSuchMethodException) {
       Log.e(TAG, "getNotchSize NoSuchMethodException")
   } catch (e: Exception) {
       Log.e(TAG, "getNotchSize Exception")
   } finally {
       return ret
   }
}

5.1.3. 应用页面设置使用刘海区显示
给window添加华为新增的FLAG_NOTCH_SUPPORT方式,代码如下所示:

/**
* 设置应用窗口在华为刘海屏手机使用挖孔区
* @param window 应用页面window对象
*/

fun setFullScreenWindowLayoutInDisplayCutout(window: Window?) {

   if (null == window) {
       return
   }

   val layoutParams: WindowManager.LayoutParams = window.attributes

   try {
       val layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx")
       val con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams::class.java)
       val layoutParamsExObj
= con.newInstance(layoutParams)
       val method = layoutParamsExCls.getMethod("addHwFlags", Int::class.javaPrimitiveType)
       method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT)
   } catch (e: ClassNotFoundException)
{
       Log.e(TAG, "hw notch screen flag api error")
   } catch (e: NoSuchMethodException) {
       Log.e(TAG, "hw notch screen flag api error")
   } catch (e: IllegalAccessException) {
       Log.e(TAG, "hw notch screen flag api error")
   } catch (e: InstantiationException) {
       Log.e(TAG, "hw notch screen flag api error")
   } catch (e: InvocationTargetException) {
       Log.e(TAG, "hw notch screen flag api error")
   } catch (e: Exception) {
       Log.e(TAG, "other Exception")
   }
}

5.2.Oppo
对于Oppo而言,它刘海的高度是固定的,就是80px。

Oppo

判断当前设备是否是刘海屏,也提供了对应的 Api,可以用以下方法获取。代码如下所示:

/**
* 判断是否是Oppo刘海屏
* @param context 上下文对象
* @return true:是刘海屏;false:非刘海屏
*/

fun hasNotchInScreenAtOppo(context: Context): Boolean {
   return context.packageManager!!.hasSystemFeature("com.oppo.feature.screen.heteromorphism")
}

返回 true 为刘海屏,但是这种方法只能识别Oppo品牌所支持的刘海屏。

5.3.Vivo
在Vivo系统中,增加了一个接口来判断此设备是否具有凹槽,我们可以使用发射的方式调用。代码如下所示:

/**
* 判断Voio是否有凹槽
*
* @param context 上下文对象
* @return true表示具备此特征,false表示没有此特征
*/

fun hasNotchInScreenAtVoio(context: Context): Boolean {
   var ret = false
   try {
       val cl = context.classLoader
       val FtFeature = cl.loadClass("android.util.FtFeature")
       val method = FtFeature.getMethod("isFeatureSupport", Int::class.javaPrimitiveType)
       ret
= method.invoke(FtFeature, NOTCH_IN_SCREEN_VOIO) as Boolean

   } catch (e: ClassNotFoundException) {
       Log.e(TAG, "hasNotchInScreen ClassNotFoundException")
   } catch (e: NoSuchMethodException) {
       Log.e(TAG, "hasNotchInScreen NoSuchMethodException")
   } catch (e: Exception) {
       Log.e(TAG, "hasNotchInScreen Exception")
   } finally {
       return ret
   }
}

6. 结语

看完本篇文章,我想你对Android P的刘海屏也有一定的认识了,现在还不确定不同厂商会不会对其微调,所以你要是碰到什么问题,欢迎一起研究学习,不妨在留言区留言讨论。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值