如何处理手势冲突 | 手势导航连载 (三)

做者 / Chris Banes, Android 开发者关系团队工程师html

咱们将在近期为你们带来一个关于 "手势导航" 的系列连载,本文是手势导航连载的第三篇,若是您但愿查看前两篇文章,请点击下方连接 :java

上一篇文章中,咱们讨论完了从边到边绘制应用内容。从这一篇文章开始咱们将介绍如何处理您的应用和 Android 10 中新引入的系统交互手势之间的冲突。android

首先让咱们来理解一下什么是 "手势冲突 (gesture conflict)"。咱们来看一个例子,好比下面这个音乐播放应用,该应用容许用户经过拖动进度条 (SeekBar) 来快进或快退当前歌曲。git

不幸的是, 进度条太靠近主屏手势区域 (Home Screen Gesture Area),因此当用户在该区域滑动时,系统把它错误地判断为用户是要执行快速切换应用的操做,这也会让用户感到困惑。支持手势导航的任何屏幕边缘区域均可能发生相似状况。有不少可能致使冲突的例子,例如: 导航抽屉 ( DrawerLayout)、多图展现 ( ViewPager)、进度条 ( SeekBar),甚至在列表上进行 滑动操做也有可能出现冲突。

那么,如何解决这个问题呢?咱们准备了一张流程图帮助你们快速作出决策:github

△ 请点击图片放大查看

注解: 非粘性沉浸模式: 用户能够经过在系统栏上滑动来退出沉浸模式。 粘性沉浸模式: 用户能够经过在系统栏上滑动来暂时退出沉浸模式bash

这里咱们向您进一步解释一下流程图里的内容。app

问题 1: 应用须要隐藏导航栏或状态栏吗?ide

流程图里的第一个问题,询问您应用的主要使用场景是否须要隐藏导航和/或状态栏。所谓 "隐藏",是指让它们根本不可见。这并不意味着让您的应用实现从边到边的全屏状态。布局

须要隐藏的缘由可能包括:优化

通常来讲,游戏、视频播放器、照片应用、绘图应用等会在这个问题中回答 "是"。

问题 2: 主要的 UI 须要在交互区域内/附近使用滑动操做吗?

这个问题是在询问,应用的界面是否在手势导航交互区域内或附近包含任何须要用户滑动操做的组件。(包括在后退和返回主屏按钮区域滑动)

很多游戏一般会在此处回答 "是",由于:

  • 游戏屏幕上的控件每每很是靠近屏幕左/右边缘,或靠近屏幕底部。
  • 某些游戏须要在屏幕上滑动操做一个元素,而这个元素可能出如今屏幕的任何位置,例如平台动做类的游戏。

除了游戏以外,有一些常见的 UI 也可能在这里回答 "是":

  • 图片裁切 UI,其中用于裁切图片的控制点可能位于屏幕左/右边缘附近。
  • 绘图应用,用户能够在屏幕画布上绘图 (天然也是滑动操做)。

问题 3: 经常使用的视图/控件位于手势交互区域内/附近吗?

这个问题应该简单一些。注意,这个问题也包括那些占据屏幕较大区域,且包括了手势交互区域的视图/控件。好比 DrawerLayout 或尺寸较大的 ViewPager。

问题 4: 该视图/控件须要滑动拖动交互吗?

这个紧接着问题 3 。在问题 3 中回答 "是" 的视图,是否须要用户在其上滑动或拖拽?

有很多用例会在本题回答 "是": 包括前面提到的进度条、底部弹出菜单 (Bottom Sheet) 或者能够经过滑动打开的弹出菜单 (PopupMenu)。

问题 5: 该视图/控件大部分位于手势交互区域内吗?

紧接着问题 4,进一步确认该视图是否彻底或大部分位于手势交互区域内。

若是您的视图放置在一个可滚动操做的容器 (如 RecyclerView) 中,那么请这么理解这个问题: 该视图是否彻底或大部分位于手势交互区域中?若是用户能够将视图滚动到手势交互区域以外,则应该视为没有交互冲突。

您也许已经注意到,在流程图中多图显示控件 (ViewPager) 在此处回答 "否"。这是由于与整个视图的宽度相比,屏幕左右侧的手势交互区域宽度相对较小 (默认为每边 20dp)。通常来讲手机竖持时屏幕宽度约为 360dp,也就是说,在约为 320dp 的范围内,用户的滑动操做不受影响 (占总宽度的近 90%)。即便考虑加上了内外边距的状况,用户仍然能够正常经过滑动操做来翻看里面的图片。

问题 6: 该视图/控件是否和强制系统手势交互区域重叠?

最后一个问题询问该控件是否位于系统强制手势导航交互区域内。若是您读过咱们以前的文章,应该会记得 "强制系统手势交互区" 是指系统手势始终被优先处理的屏幕区域。

对 Android 10 来讲,强制交互区域只有一个,那就是屏幕底部。该区域内的滑动操做能让用户返回主屏或访问最近使用的其余应用。这个强制交互区域可能会在未来的平台版本中发生变化,但如今咱们只须要考虑屏幕底部便可。

出现这种重叠的常见的例子:

  • 非模态的底部弹出菜单,由于这种菜单经常会在屏幕底部折叠为一个较小的视图,并且还须要滑动操做。
  • 屏幕底部的水平页面切换,例如软键盘里选择不一样表情包的 UI。

OK,如今我已经解释了流程图中的问题,下面咱们来详细说说流程图中给出的解决方案。

解决方案 1: 无需处理手势冲突

最简单的 "解决方案" ,只须要……什么都不作!

固然,也许您还能够 (参考接下来的几种解决方案) 作点优化,但在启用了手势导航的应用中,您应该不会遇到大问题。

若是流程图为您选择了 "什么都不作" 的答案,但您依然以为应用的使用有问题,请务必反馈给咱们

解决方案 2: 将该视图/控件移出手势交互区域

咱们在上一篇文章有提到,能够用 Insets 区域来告知应用系统手势区域在屏幕中的位置。咱们能够用来解决手势冲突的一种方法是,将出现冲突的视图移出手势导航交互区域。这对于屏幕底部附近的视图尤为重要,由于该区域是系统强制手势交互区域,而且应用没法在该区域使用热区切出 API。

这里让咱们回到以前提到的音乐播放器示例。它包含一个位于屏幕底部的进度条,容许用户快进和快退歌曲。

可是,当用户尝试快进和快退歌曲时,会发生这种状况:

发生这种状况是由于,屏幕底部的系统手势交互区域与进度条重叠了,而在这里系统手势优先级更高。系统手势区域以下图所示:

△ 从蓝色区域向屏幕中间滑动至关于 "返回" 按钮;从红色区域向上滑动则是返回主屏,注意红色区域即为系统强制手势交互区域

简单的解法

这个问题最简单的解决方案是,添加一些内/外边距,将进度条向上推到手势区域以外。就像这样:

△ 进度条向上移动后再也不出现冲突

为了实现这一点,咱们须要使用 API 29 和 Jetpack Core 库 v1.2.0 (当前为 alpha 版) 中提供的新系统交互热区 API。以下方代码,咱们给进度条增长了底边距,增长的值正好是系统强制交互区的高度:

ViewCompat.setOnApplyWindowInsetsListener(seekBar) { view, insets ->
    // We'll set the views bottom padding to be the same // value as the system gesture insets bottom value view.updatePadding( bottom = insets.systemGestureInsets.bottom ) insets } 复制代码

您也能够查阅咱们发布的另外一篇博文,咱们在那里探讨了一些让 WindowInsets 更易于使用的方法。

更优的解法

在作完上一步后,您可能会以为问题已经解决了。对于某些布局,这极可能是最终解决方案。可是在上面的修改后,进度条下方有不少空间被浪费掉了,使得 UI 在外观上的完成度降低。所以,除了直接修改视图的边距,咱们还能够修改布局,以免出现空间浪费:

△ 将进度条移到视图的顶部
在这里,咱们将进度条移到了播放控件的顶部,彻底移出了手势交互区域。并且这样作还使得咱们再也不须要额外插入太多无用的边距。

但请注意,咱们依然须要在播放控件底部插入一个内边距,其值等于系统栏的高度,这样可使歌曲名称等文本不会被系统导航条 (即屏幕底部的那条 "横线") 遮盖。

解决方案 3: 使用手势区域排除 API

咱们在上一篇文章中有提到 "应用能够从系统手势区域中切出一部分用来响应本身的手势交互"。这就是 Android 10 中新引入的手势区域排除 API。

应用能够经过 Android 10 中新增的系统手势区域排除 API 来让系统边缘的一部分区域不响应系统手势。系统提供了两种不一样的功能来 "切出" 交互区域: View.setSystemGestureExclusionRects() Window.setSystemGestureExclusionRects()。使用哪一种取决于您的应用: 若是您使用的是 Android View,则建议首选 View API,不然请使用 Window API。

这两个 API 之间的主要区别在于,Window API 会以窗口 (Window) 坐标系计算矩形。若是使用的是 View API,则会以视图的坐标系进行操做。View API 会帮您解决坐标空间之间换算的问题。

让咱们再次回到以前提到的音乐播放器示例,咱们如今把播放进度条挪到了控件上方,而且撑满了整个屏幕宽度。这时屏幕底部的系统手势交互冲突已经解决了,但屏幕左右两侧的 "后退" 操做依然和进度条有冲突:

在上图中,因为进度条的播放头正好位于右侧手势区内,所以系统认为用户正在用手势执行 "返回" 操做,所以显示了 "向后" 的箭头。这时就会让用户感到困惑,由于他们可能并不想后退。出现这种冲突时,咱们就可使用上面提到的手势区域排除 API 来解决。

手势区域排除 API 一般会在两个地方被调用: 当视图被布局时 (onLayout),或是当视图被绘制时 (onDraw)。您的视图会传入一个 List ,其中包含应该切出 (即不响应系统手势) 的矩形区域。如前所述,这些矩形须位于视图本身的坐标系中。

一般,您会建立一个相似于下面的方法,该方法会在 onLayout() 和/或 onDraw() 时被调用:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+ if (Build.VERSION.SDK_INT < 29) return // First, lets clear out any existing rectangles gestureExclusionRects.clear() // Now lets work out which areas should be excluded. For a SeekBar this will // be the bounds of the thumb drawable. thumb?.also { t -> gestureExclusionRects += t.copyBounds() } // If we had other elements in this view near the edges, we could exclude them // here too, by adding their bounds to the list // Finally pass our updated list of rectangles to the system systemGestureExclusionRects = gestureExclusionRects } 复制代码
  • 上例的完整代码:

gist.github.com/chrisbanes/…

作完这个 "切出" 操做后,在屏幕边缘附近进行快进/快退操做就没有问题了:

注意: SeekBar 实际上会在 Android 10 中自动为您执行上述切出操做,所以您无需在 Seekbar 中这么作。这里只是做为示例向您展现处理冲突的作法。

限制条件

尽管手势区域排除 API 彷佛是解决全部手势冲突的完美方案,但实际上并不是如此。经过使用这个 API,您实际上在声明应用的手势比 "返回" 等系统操做更重要。这个作法咱们只建议您在没有其余解决方案时采用。

因为这个 API 会必定程度上破坏用户习惯的操做,所以系统作出了限制: 屏幕的每一个边缘最多只能被应用切除 200dp。

开发者听到这个限制时,常会提出如下问题:

为何要有限制?

咱们认为,开发者须要尽可能确保用户使用一致的操做来与系统进行交互,如从边缘向内滑动进行返回。注意是在整个设备上,而不只仅是在一个应用中保持一致性。这个限制看似严厉,但若是一个应用可以让屏幕的整个边缘都不响应系统手势,就会让用户感到困惑,这个应用也极有可能被用户卸载。

再次强调,系统导航必须始终保持一致性和可用性。

为何是 200dp?

200dp 背后的决策逻辑很是简单。正如咱们前面提到的,手势区域排除 API 只有在万不得已的状况下才可使用,所以咱们计算了可能须要应用这套机制的触摸对象的面积。触摸对象的最小推荐尺寸是 48dp。咱们取 4个触摸对象,即 4 × 48dp = 192dp。再加入一点富余量,即为 200dp。

若是开发者要求在边缘上切出 200dp 以上的区域会怎样?

答案是,系统只会兑现您的要求中位于最下方的 200dp,以下图所示:

△ 开发者请求切出 50 + 50 + 125 + 50 dp 的区域,但系统只兑现最下面的总计 200dp

个人视图不在屏幕内,是否也会受到这个限制?

不会,系统仅计算屏幕范围内的切出矩形。一样,若是视图只有一部分显示在屏幕内,则仅计算所请求矩形的屏幕内可见部分。

请关注下一篇连载

读完本文您可能会问: 为何咱们尚未讲流程图的右半部分?这是由于右半部分适用于那些须要全屏绘制内容的应用,咱们将在下一篇手势导航连载中为您继续讲解,敬请保持关注。

点击这里进一步了解 Android 手势导航

相关文章
相关标签/搜索