WindowInsetsCompat的浅识

157ad32a07dc6eb3d12deb06b42581fa.gif

1.什么是window

了解WindowInsets之前,我们先复习一下什么是Window?那么由一个问题开始:请问一个android APP 一定要有Activity才能展示界面吗? Five minutes later~~~ 答案是不一定的(可以百度一下”如何创建没有activity的app” 会有很多搜索结果告诉你怎么做……),那么我们就得知了Android 中 Activity 不是显示 UI 的必要条件,那什么才是呢? 那就是Window, 它可以说是Android Framework层提供的一个最基础的UI组件管理类, PhoneWindow是它的唯一实现类。Activity/Dialog/Toast的UI展现都是依赖于Window来完成。对于UI编写,开发者只需要使用View相关,View最终会以ContentView的形式设置给Window,所以Window就是用来承载和管理View的, 所有视图都是通过Window来实现的,View不能单独存在,必须依附window这个抽象概念。

2.什么是Insets

Insets中文是插入物, 屏幕上除了开发者 app 绘制的内容还有系统的插入物,比如:顶部的状态栏,底部的虚拟导航栏,软键盘等. 如果开发者绘制的内容与系统UI区域重合了,就可能出现被系统 UI 遮盖,视觉与手势的冲突等现象。开发者可以借助 Insets 把所要绘制的View 从屏幕边缘向内移动到一个合适的位置。

注意:不要把 Insets 的 top ,bottom,left,right 与 Rect 的搞混,前者描述的是偏移,后者是坐标。在 Android 上,Insets 区域由 WindowInsets 类表示,在 AndroidX 中则使用 WindowInsetsCompat。

3. 什么是WindowInsets

分别了解了Window和Insets之后,就该来说说WindowInsets了。WindowInsets就是描述窗口内容的一组插入(个人理解就是用来获取系统控件的位置然后来适配我们自己View的位置的), WindowInsets如下图:

125816151d804db87d139f95bcf6ff4c.png

大家可以看到这里面最主要的有以下几个变量mStableInsets,mSystemWindowInsets,mWindowDecorInsets

StableInsets:全窗⼝下,被系统UI覆盖的区域(稳定显示边衬区域)

SystemWindowInsets:全窗⼝下,被navigationbar、statusbar、ime或其他系统窗⼝覆盖的区域(最常用)

WIndowDecorInsets:系统预留属性

  • insets. systemWindowInsets

系统窗口区域是最常用到的。例如: 如果最底部有个底部导航栏, 在迭代为全面屏应用后,为了取得更加沉浸式的体验,我们将界面延展进了系统导航的区域。但这时可以想象的到,底部的导航栏肯定会被系统的虚拟导航栏遮住了,同时也会影响它的点击等操作,如下图

98ab3e90ea87f4b64d7f3a40245a1b04.png

图1 

这个时候我们就可以使用 systemWindowInsets.bottom值来增加导航栏下方的边距。为了使用 WindowInsets,我们通常需要在一个视图上添加OnApplyWindowInsetsListener,并且在这个函数中处理传进来的边衬区,代码如下,处理结果如图2

ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
           val sysWindow = insets.systemWindowInsets
           //val stable = insets.stableInsets
           //val systemGestures = insets.systemGestureInsets
           //val tappableElement = insets.tappableElementInsets
           v.updatePadding(bottom = sysWindow.bottom)
// 返回边衬区,这样它们才能够继续在视图树中继续传递下去
           insets
       }

6b3d1142a1cde1fe861591f8469b78f5.png

图2

但是目前许多 WindowInsets API 已经被弃用了,取而代之的一些新函数来查询不同类型的边衬区:

getInsets(type: Int) 会返回指定类型的可见边衬区。

getInsetsIgnoringVisibility(type: Int) 会返回所有边衬区,无论它们是否可见。

isVisible(type: Int) 如果指定的类型是可见的,会返回 true,反之则会返回false。

ViewCompat.setOnApplyWindowInsetsListener(...) { view, insets ->

-    val sysWindow = insets.systemWindowInsets

+    val sysWindow = insets.getInsets(Type.systemBars())

-    val stable = insets.stableInsets

+    val stable = insets.getInsetsIgnoringVisibility(Type.systemBars())

-    val systemGestures = insets.systemGestureInsets

+    val systemGestures = insets.getInsets(Type.systemGestures())

-    val tappableElement = insets.tappableElementInsets

+    val tappableElement = insets.getInsets(Type.tappableElement())

}

那么刚刚提到的这些方法里面的参数Type是什么呢?

它们在 WindowInsetsCompate.Type 类中被定义为函数,每个函数都会返回一个整数标示,他们分别是

WindowInsetsCompat.Type.ime();//键盘

WindowInsetsCompat.Type.statusBars();//状态栏
WindowInsetsCompat.Type.navigationBars();//导航栏
WindowInsetsCompat.Type.captionBar();//标题栏
WindowInsetsCompat.Type.systemBars();//状态栏,导航栏和标题栏

WindowInsetsCompat.Type.systemGestures();//系统手势
WindowInsetsCompat.Type.mandatorySystemGestures();//强制性系统手势
WindowInsetsCompat.Type.tappableElement();//可点击区域
WindowInsetsCompat.Type.displayCutout();//刘海屏

其中不太好理解的:

  • WindowInsetsCompat.Type.systemGestures()

这个指的是Android 10 带来的新的手势导航模式,允许用户通过手势动作,而不是导航按钮来进行导航,比如从屏幕左/右边缘向中间滑动,相当于后退按钮等

c13867fb34b1acbea771101901624e12.png

图3

  • WindowInsetsCompat.Type.mandatorySystemGestures()

强制系统手势边衬区是系统手势边衬区的子集,之所以称之为 "强制区域",是因为应用无法修改这些区域。强制系统手势边衬区只包含那些系统保留的区域,在这些区域内系统手势操作永远优先。在 Android 10 上,当前唯一的强制区域是屏幕底部的主屏手势区域,系统保留这个区域就可以让用户在任何时候都可以退出当前应用(底部 60dp 即为强制系统手势边衬区(如上图底部藕粉色区域)

  • WindowInsetsCompat.Type.tappableElement()

可点击区域边衬区也是 Android 10 中新增的可点击区域 insets。它与上面的系统窗口区域 insets 非常相似。它用来界定可触发系统点击行为 (tap) 的最小区域。注意,使用可点击区域里的数值进行布局时,依然可能导致自己的控件与系统 UI 在视觉上重叠,这一点与系统窗口区域 insets 不同,使用后者的值对自己的控件进行位移后能确保不会与系统/导航栏发生视觉重叠。比如:刚刚我们自己的底部导航条在导航栏模式下,用此type获取到的bottom正常,可以上移到如图2的效果,但是在手势操作 (导航条) 模式,且开启了导航条色彩适应后,虽然导航条依然有高度即图3底部的效果,但它被认为是 "透明" 的,系统在这高度内依然允许用户点击应用里的控件,所以在可点击区域 insets 中,其 bottom 值为 0dp,这就会导致应用内部的UI仍可能会与系统导航在展示上发生重叠,但不影响点击效果的情况发生

所以从适配的角度上讲,我建议大家用系统窗口区域 insets(systemWindowInsets),它可以更好地满足几乎所有需要使用可点击区域 insets 的用例。

4. 如何处理边衬区的冲突显示

在对不同类型的 insets 区域有了了解之后,下面我们来看看您需要如何在应用中实际使用它们(其实上面已经说过,哈哈),我们访问 WindowInsets主要就是通过 setOnApplyWindowInsetsListener这个方法进行,我们想要给某个控件增加一下下边距以适配手机的底部的虚拟导航,代码如下

e38c25cc188c62ce6e223080b640cf77.png

注意 这个回调可能会调用多次

  • WindowInsetsCompat.Type.ime()

软键盘类型

应用使用过程中 我们可以获取到关于软键盘的一些信息,如下:

val insets = ViewCompat.getRootWindowInsets(view)
val imeVisible = insets.isVisible(WindowInsets.Type.ime())
val imeHeight = insets.getInsets((WindowInsets.Type.ime()).bottom

如果我们需要监听软键盘的改变,我们可以照常使用 OnApplyWindowInsetsListener,并且使用同样的函数:

ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->

val imeVisible = insets.isVisible(WindowInsets.Type.ime())

val imeHeight = insets.getInsets(WindowInsets.Type.ime()).bottom

}

我们还可以通过WindowInsetsAnimation让应用随着软键盘一起移动,创建更加无缝的体验, 它是Android 11提供的API,该类封装了包含插图的动画。应用程序可以通过WindowInsetsAnimation.Callback类收听动画事件

object :WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP){

   override fun onPrepare(animation: WindowInsetsAnimation) {
       super.onPrepare(animation)
       //将在发生任何布局更改之前记录view底部坐标
   }

   override fun onProgress(
       insets: WindowInsets,
       runningAnimations: MutableList<WindowInsetsAnimation>
   ): WindowInsets {
            //动画进行时
       return insets
   }

   override fun onEnd(animation: WindowInsetsAnimation) {
       super.onEnd(animation)
       //动画结束后可以重置
   }
}

关闭/隐藏 Android 软键盘(注: 需要界面有EditText并且EditText获取到焦点才能起作用)

private fun showSoftKeyboard(view: View) {
   isClose = false
ViewCompat.getWindowInsetsController(view)!!.show(WindowInsetsCompat.Type.ime())
}

private fun hideSoftKeyboard(view:View) {
   isClose = true
ViewCompat.getWindowInsetsController(view)!!.hide(WindowInsetsCompat.Type.ime())
}

控制状态栏以及文字颜色

val controller: WindowInsetsControllerCompat? = ViewCompat.getWindowInsetsController(view);
//隐藏状态栏controller?.hide(WindowInsetsCompat.Type.statusBars())
//显示状态栏
controller?.show(WindowInsetsCompat.Type.statusBars())
//设置状态栏颜色为黑色/白色
controller?.isAppearanceLightStatusBars = false

控制导航栏

//隐藏导航栏
controller?.hide(WindowInsetsCompat.Type.navigationBars())
//显示导航栏
controller?.show(WindowInsetsCompat.Type.navigationBars())
//导航栏隐藏时,这三种手势可以唤醒导航栏出现
/**
* BEHAVIOR_SHOW_BARS_BY_SWIPE
* BEHAVIOR_SHOW_BARS_BY_TOUCH
* BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
*/
controller?.systemBarsBehavior=WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE

5、总结

总的来说这个WindowInsetsCompat以及相关API的出现,实现了window控件的简单化,使获取,管理键盘,状态栏,导航栏等系统控件的操作更加直接,简洁,适配的更加方便,希望以上对它简单的介绍可以让大家对它有个初步的认识!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值