前言
其实Android 9.0系统已经是去年推出的“老”系统了,这个系统中新增了一个比较重要的特性,就是对刘海屏设备进行了支持。一直以来我也都有打算针对这个新特性好好地写一篇文章,但是为什么直到拖到了Android 10.0系统都发布了才开始写这篇文章呢?当然,一是因为我这段时间确实比较忙。但是最主要的原因并不是这个,而是因为刘海屏设备的适配存在一定的特殊性。
我先来带着大家回顾一下手机屏幕的发展历史。在之前的很长一段时间里,绝大多数的手机屏幕使用的都是16:9的比例。当时普遍认为16:9就是最合适的设备屏幕比例,因为手机上方还要给听筒摄像头留足空间,下方还要给Home键留足空间。然而,根据我所能查到的最早资料,小米是第一个敢于打破这个限制的手机厂商(不保证一定正确)。在2016年的时候,小米推出了MIX一代手机,将屏幕做到了接近18:9的比例,并首次提出了全面屏的概念。但是要做到真正的全面屏并不是一件容易的事情,像Home键之类的实体按键还可以用虚拟按键来替代,但是前置摄像头这种就是实打实的硬件传感器了,必须得要占据一定的空间。因此,小米MIX选择了将摄像头做到了屏幕下方,形成了一个比较宽的下巴。
适配场景
如果App使用了沉浸式的状态栏,或者透明状态栏,我们自己的布局延伸到了状态栏内部,这时候如果我们在刘海处有一个可交互的控件就会被遮挡使用了全面屏的页面,比如APP的闪屏界面,图片查看大图的页面,这时候状态栏不可见,刘海会遮挡一部分地方。常见刘海屏样式
刘海模拟
常见的刘海大概就是以上这几种吧,但是看到这里你可能会犯难了,我到哪里去找这么多不同种类的刘海屏手机来进行测试呢?不用担心,只要你手上有任何一部Android 9.0或以上系统的手机,都是可以模拟出各种不同类型的刘海的。当然,即使你手上没有任何一部手机,也可以正常进行测试,只需要借助Android官方的模拟器即可。
这里我创建了一台Android 10.0系统的模拟器,语言选择中文简体,在开发者选项当中,将可以找到刘海屏这个栏目,如下图所示。
第一个默认是无刘海,接下来的每一个选项都可以模拟出一种不同的刘海模式,我们可以把每个选项都点一点,比如说下图对应的就是“张雨绮”式的刘海。
而下图对应的是“桂纶镁”式的刘海。
最后,下图对应的是混合模式的刘海。
了解了各种不同的刘海模式,以及其对应的模拟方式,这样我们就将准备工作都完成了,接下来终于可以进入到具体的编码适配环节了。
思考一下,其实对于刘海屏的适配并不应该是一件复杂的事情,因为我们的目标很简单,就是不要让刘海部分遮挡到应用程序,或者影响到应用程序的正常使用即可。
为此,Android 9.0系统中提供了3种layoutInDisplayCutoutMode属性来允许应用自主决定该如何对刘海屏设备进行适配。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:这是一种默认的属性,在不进行明确指定的情况下,系统会自动使用这种属性。这种属性允许应用程序的内容在竖屏模式下自动延伸到刘海区域,而在横屏模式下则不会延伸到刘海区域。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:这种属性表示,不管手机处于横屏还是竖屏模式,都会允许应用程序的内容延伸到刘海区域。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:这种属性表示,永远不允许应用程序的内容延伸到刘海区域。
代码实现
了解了以上内容之后,接下来我们就可以动手进行实现了。首先创建一个Demo项目,并让Android Studio帮我们自动生成一个空的Activity。在不编写任何额外代码的情况下直接运行该项目,效果如下图所示。
可以看到,在竖屏模式下应用程序的状态栏部分刚好占据了手机的刘海区域,并且系统还会根据刘海的高度来自动调整状态栏的高度,这样应用程序中的内容自然是不会被刘海部分遮挡掉的。
现在如果我们旋转一下手机,横屏模式下的效果如下图所示。
这个时候,手机的刘海区域会整个变成一条大黑边,应用程序的内容是不允许延伸到这部分区域里的,这样也不会产生内容被遮挡的情况。
也就是说,即使我们不做任何的适配工作,绝大多数的程序在默认情况下也是可以自动适配刘海屏手机的,并不会产生应用程序无法使用等问题的发生。但是,假如你开发的是一款视频类应用或者游戏的话,充分利用屏幕的空间明显可以带来更好的用户体验,界面上留着一条大黑边对用户总归是不够友好的。这个时候我们就可以通过指定layoutInDisplayCutoutMode属性的值,来让应用程序具备更好的屏幕适配性。
这里我就使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES属性,并且配合着沉浸式模式的代码,来编写一个全屏的UI界面,以此模拟视频和游戏类App的效果。
首先为了防止界面出现一片空白的情况,我对activity_main.xml布局的内容进行了修改,如下所示:
<?xml version="1.0" encoding="utf-8"?>
这里给最外层的FrameLayout指定了一个背景图,随便使用什么图片都可以,我们只是为了便于进行演示。然后添加两个按钮,用于模拟操作按钮被刘海遮挡
接下来修改MainActivity中的代码,为其指定LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES属性。另外,为了让界面效果更加贴近于视频应用或游戏,这里我将MainActivity调整成了沉浸式模式,代码如下所示:
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE) window.setFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN ) setContentView(R.layout.activity_main) window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN supportActionBar?.hide() if (Build.VERSION.SDK_INT >= 28) { window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES } } override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (hasFocus) { window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } }}
其实这段代码需要我们关心的就是if (Build.VERSION.SDK_INT >= 28)的这个逻辑判断中的内容,在这里我们将当前Activity的layoutInDisplayCutoutMode属性指定成LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,这样就可以让应用程序的内容延伸到刘海区域了。
现在重新运行一下程序,效果如下图所示。
可以看到,程序进入了全屏沉浸式体验的效果,并且我们在布局文件中设置的背景图是可以延伸到刘海区域的,这就使得手机屏幕的空间得到了更充分的利用。
现在旋转一下手机屏幕,效果如下图所示:
很明显,这比之前在刘海区域空出一条大黑边的用户体验要好上太多了。
不过,虽然现在我们已经实现了让应用程序的内容延伸到刘海区域的功能,却无法保证刘海部分不会影响到应用程序的正常使用。正如上图所看到的一样,沉浸式全面屏带来的问题就是会有一部分操作按钮被遮挡,如果这是一款游戏,那可能这款游戏就完全没法玩了。
因此,对于任何应用程序或者是游戏而言,都需要在这方面进行适配,保证自己的可交互控件绝对不能被刘海区域遮挡住。
那么具体应该如何实现这个功能呢?Android在9.0系统中提供了一套专门用于获取安全显示区域的API,我们只需要确认出哪些位置是有可能被遮挡到的,然后对可交互控件进行相应的位置偏移就可以了,示例代码添加到onCreate()方法中,如下所示:
if (Build.VERSION.SDK_INT >= 28) { mRootCl?.setOnApplyWindowInsetsListener { _, insets -> if (insets.displayCutout!=null){ val left = insets.displayCutout?.safeInsetLeft val top = insets.displayCutout?.safeInsetTop val right = insets.displayCutout?.safeInsetRight val bottom = insets.displayCutout?.safeInsetBottom (mOneBtn?.layoutParams as ConstraintLayout.LayoutParams).setMargins(left!!, top!!, right!!, bottom!!) (mTwoBtn?.layoutParams as ConstraintLayout.LayoutParams).setMargins(left, top, right, bottom) } insets.consumeSystemWindowInsets()
这段代码并没有什么难理解的地方,和刚才所讲解的示例代码是差不多的。只是在得到上下左右方向上的偏移距离之后,我们通过给按钮的layout设置margin的方式来让控件在四个方向上进行相应的偏移。如果你是在开发游戏的话,也可以同样套用这段代码,只是在获取到相应的偏移距离之后,将这几个值传递给游戏层逻辑即可,由游戏层来控制如何对可交互的控件进行偏移。
现在来重新运行一下代码吧,竖屏模式下的结果如下图所示。
可以看到,顶部可交互控件自动向下偏移了一段距离,刚好可以保证不被刘海区域遮挡到。
那么再来看一下横屏模式下的结果吧,如下图所示。
没有问题,横屏模式下侧边可交互控件自动向右偏移了一段距离,从而也不会被刘海区域遮挡到了。
不过你会发现,在横屏模式下,顶部可交互控件并没有处于屏幕中间的位置,这是因为屏幕的左侧存在刘海,因此DisplayCutout会告诉我们要向左偏移一定的距离。但是我们并没有判断哪些控件需要偏移,哪些控件不需要偏移,而是直接将所有控件都进行偏移,才出现了这种没有居中对齐的情况。
至于解决办法其实并没有什么简单的方式,就是增加逻辑判断即可,在横屏模式下我们可以断定顶部可交互控件是绝对不可能被刘海遮挡到的,因此只需要对侧边可交互控件进行偏移即可,具体的代码我就不再进行演示了。
总结
虽然整篇文章我都一直在使用张雨绮”式的刘海来进行演示,但是我们所使用这种适配方案,是可以保证在任何刘海模式下,你的可交互控件都不会被遮挡到的,感兴趣的话你可以自行切换成其他的刘海模式来进行更多的测试。
好的,关于Android 9.0系统刘海屏的适配就讲到这里,相信你已经可以完全掌握了。