做者 / Chris Banes, Android 开发者关系团队工程师html
咱们将在近期为你们带来一个关于 "手势导航" 的系列连载,本文是连载的第二篇,若是您但愿了解其余手势导航的话题,请持续关注咱们。android
在上一篇文章中,咱们介绍了如何将应用构建到全面屏设备。然而有些交互可能致使应用的某些视图被系统栏遮盖,致使用户没法看见或操做。本文正是为帮助您解决这个问题而撰写——如何判断安全的交互区域。安全
更具体一点来讲,本文主要处理与系统 UI 出现视觉重叠的问题。系统 UI 包括屏幕上由系统提供的全部 UI,例如导航栏和状态栏,另外它还包括诸如通知面板之类的内容。bash
很多 Android 开发者看到边衬区 (insets) 每每会远而避之,这个可能来源自他们在 Android Lollipop 时代试图在状态栏后面绘制 UI 的经历,而这个经历并不那么使人愉悦。咱们甚至能看到在 StackOverflow 上有个一直热门的问题就是关于这个的。app
Insets 区域负责描述屏幕的哪些部分会与系统 UI 相交 (intersect),例如导航或状态栏。若是您的控件出如今了这些区域内,就可能被系统 UI 遮盖。天然,咱们可使用 insets 区域来尝试解决视觉冲突,如把视图从屏幕边缘向内移动到一个合适的位置。ide
在 Android 上,Insets 区域由 WindowInsets 类表示,在 AndroidX 中则使用 WindowInsetsCompat。在 Android 10 系统中处理应用布局时,开发者须要知晓 5 个获取 insets 区域的方法。须要使用哪一种方法取决于具体状况,接下来就让咱们逐一说明。布局
方法: getSystemWindowInsets()测试
系统窗口区域是最经常使用到的。自 API 1 以来,它们就以各类形式存在着,而且每当系统 UI 重叠显示在您的应用上方时,这个方法就会被调用。常见的例子是下拉状态栏和导航栏,或者弹出屏幕软键盘 (IME)。ui
咱们来看一个使用系统窗口区域的例子。咱们有一个悬浮操做按钮 (FAB),它位于屏幕右下角,距离屏幕边缘 16dp (这符合设计指南中的要求)。google
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_margin="16dp"
android:layout_gravity="bottom|end" />
复制代码
Google I/O 的官方应用中就有这种 FAB,在应用被迭代为全屏应用前它看起来是这个样子:
再强调一次,您如今最好在全部的导航模式下测试您的应用。
那么咱们如何处理这种视觉冲突呢?系统窗口区域在这就能派上用场。这套 insets 描述了系统栏占据的区域,方便您使用对应的数值将本身的控件从系统栏下面移开。
具体到本例中,FAB 位于底部右侧边缘附近,所以咱们可使用 systemWindowInsets.bottom 和 systemWindowInsets.right 值来增长 FAB 下方和右方的边距。
增长边距后看到的效果以下:
简而言之,系统窗口区域 insets 最适合那些须要点击的控件,能够确保系统栏不遮盖住它们。
方法: getTappableElementInsets()
接下来是 Android 10 中新增的可点击区域 insets。它们与上面的系统窗口区域 insets 很是类似。可点击区域 insets 用来界定可触发系统点击行为 (tap) 的最小区域。注意,使用可点击区域里的数值进行布局时,依然可能致使本身的控件与系统 UI 在视觉上重叠,这一点与系统窗口区域 insets 不一样,使用后者的值对本身的控件进行位移后能确保不会与系统/导航栏发生视觉重叠。
这里让咱们仍然使用 FAB 来举例:
不要在代码中硬编码上面提到的值 (48dp / 16 dp),由于导航栏的尺寸是会变更的,请使用 insets 获取须要的数值。
Insets 其实并无规定 "您应在何处放置本身的控件",因此从理论上讲能够这么作:
从实用的角度出发,在平常开发中我建议使用系统窗口区域 insets,它能够更好地知足几乎全部须要使用可点击区域 insets 的用例。
方法: getSystemGestureInsets() & getMandatorySystemGestureInsets()
这是在 Android 10 中新增的: 系统手势区域边衬区 (insets)。Android 10 带来了新的手势导航模式,容许用户经过手势动做,而不是导航按钮来进行导航:
系统手势边衬区
首先是系统手势边衬区。在这些区域内,系统手势优先于应用手势。在 Android 10 上,系统手势区域以下:
强制系统手势边衬区
强制系统手势边衬区是系统手势边衬区的子集,之因此称之为 "强制区域",是由于应用没法修改这些区域 。关于如何修改系统手势区域,请参考咱们接下来的文章《如何处理手势冲突 | 手势导航连载 (三)》。
强制系统手势边衬区只包含那些系统保留的区域,在这些区域内系统手势操做永远优先。在 Android 10 上,当前惟一的强制区域是屏幕底部的主屏手势区域,系统保留这个区域就可让用户在任什么时候候均可以退出当前应用:
方法: getStableInsets()
这也是咱们今天提到的 5 个 inset 方法的最后一个。严格来讲,这个方法与手势导航关系不大,可是为了知识的完整性,咱们这里快速介绍一下这个方法。
和系统窗口边衬区相似,稳定显示区域是系统 UI 可能在您的应用上显示的位置。在有些显示模式下 (好比放松模式和沉浸模式),系统 UI 可能会根据状况在可见与不可见之间切换 (如游戏、照片浏览、视频播放器等)。这时使用稳定显示区域就能够确保本身的控件不会被 "忽然出现" 的系统 UI 挡住。
但愿您如今对不一样类型的 insets 区域有了更深的了解,下面咱们来看看您须要如何在应用中实际使用它们。
访问 WindowInsets 主要是经过 setOnApplyWindowInsetsListener 这个方法。咱们来看一下例子,咱们想给某个控件增长一些边距,让它不被导航栏遮挡:
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.updatePadding(bottom = insets.systemWindowInsets.bottom)
// Return the insets so that they keep going down the view hierarchy
insets
}
复制代码
在这里,咱们仅将系统窗口区域的底部边距值赋给了控件的底边距。
注意: 若是您要在 ViewGroup 上执行此操做,则可能要对其进行设置 android:clipToPadding="false"。这是由于默认状况下,全部视图都会在填充区域内裁剪图形。该属性一般与 RecyclerView 一块儿使用,咱们将在之后的文章中对其进行详细介绍。
可是,请确保 Listener 里的计算操做有幂等性,即屡次进行该计算所获得的结果应该相同。如下是一个错误的例子:
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
v.updatePadding(bottom = v.paddingBottom + insets.systemWindowInsets.bottom)
insets
}
复制代码
请不要在计算边距时使用自加运算 (+=)。由于这个计算可能会重复屡次,自加运算会致使结果不符合预期。
使用 insets 时,我建议始终用 Jetpack 中的 WindowInsetsCompat 类,不管您须要的最低 SDK 版本是什么。多年来,WindowInsets API 已获得改进和扩展,而 compat 版本在全部的 API 级别上都提供了一致的 API 和行为。
在 Android 10 中新增的 insets 方面,compat 版本的方法在全部 API 级别的设备上都能获得正确的结果。要访问 AndroidX 中的新 API,请确保更新到 androidx.core:core:1.2.0-xxx (目前为 Alpha 版) 或更高版本。
本文提到的是使用 WindowInsets[Compat] API 的最简单方法,但它们可能会让您的代码很是冗长和重复。我在今年早些时候写了一篇博文,详细介绍了一些使用绑定转换操做显著提升效率的作法。
在本次连载的下一篇文章《如何处理手势冲突 | 手势导航连载 (三)》中,咱们将为你们介绍如何处理应用与系统的手势导航冲突,敬请保持关注。
点击这里进一步了解 Android 手势导航