如何处理手势冲突 | 手势导航连载 (三)
作者 / Chris Banes, Android 开发者关系团队工程师
不幸的是, 进度条太靠近主屏手势区域 (Home Screen Gesture Area),所以当用户在该区域滑动时,系统把它错误地判断为用户是要执行快速切换应用的操作,这也会让用户感到困惑。支持手势导航的任何屏幕边缘区域都可能发生类似情况。有很多可能导致冲突的例子,例如: 导航抽屉 (DrawerLayout)、多图展示 (ViewPager)、进度条 (SeekBar),甚至在列表上进行滑动操作也有可能出现冲突。
DrawerLayout https://developer.android.google.cn/reference/androidx/drawerlayout/widget/DrawerLayout.html ViewPager https://developer.android.google.cn/reference/androidx/viewpager2/widget/ViewPager2.html Seekbar https://developer.android.google.cn/reference/android/widget/SeekBar.html 了解滑动操作 https://material.io/design/components/lists.html#behavior
那么,如何解决这个问题呢?我们准备了一张流程图帮助大家快速做出决策:
* 注解
非粘性沉浸模式: 用户可以通过在系统栏上滑动来退出沉浸模式。
粘性沉浸模式: 用户可以通过在系统栏上滑动来暂时退出沉浸模式
问题 1: 应用需要隐藏导航栏或状态栏吗?
需要隐藏的原因可能包括:
使用 FLAG_FULLSCREEN WindowManager 开关。注意,这个效果也可以通过 android:windowFullscreen 主题设置来实现,或者扩展一个 Theme.XXX.Fullscreen 的衍生控件。
使用 SYSTEM_UI_FLAG_FULLSCREEN 这个系统 UI 可见性开关。
使用沉浸模式中的系统 UI 可见性开关: SYSTEM_UI_FLAG_IMMERSIVE 或 SYSTEM_UI_FLAG_IMMERSIVE_STICKY。
FLAG_FULLSCREEN
https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN
android:windowFullscreen
https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN
SYSTEM_UI_FLAG_FULLSCREEN
https://developer.android.google.cn/reference/android/view/View.html#SYSTEM_UI_FLAG_FULLSCREEN
SYSTEM_UI_FLAG_IMMERSIVE
https://developer.android.google.cn/reference/android/view/View.html#SYSTEM_UI_FLAG_IMMERSIVE
SYSTEM_UI_FLAG_IMMERSIVE_STICKY
https://developer.android.google.cn/reference/android/view/View.html#SYSTEM_UI_FLAG_IMMERSIVE_STICK
一般来说,游戏、视频播放器、照片应用、绘图应用等会在这个问题中回答 "是"。
这个问题是在询问,应用的界面是否在手势导航交互区域内或附近包含任何需要用户滑动操作的组件。(包括在后退和返回主屏按钮区域滑动)
游戏屏幕上的控件往往非常靠近屏幕左/右边缘,或靠近屏幕底部。
某些游戏需要在屏幕上滑动操作一个元素,而这个元素可能出现在屏幕的任何位置,例如平台动作类的游戏。
图片裁切 UI,其中用于裁切图片的控制点可能位于屏幕左/右边缘附近。
绘图应用,用户可以在屏幕画布上绘图 (自然也是滑动操作)。
这个问题应该简单一些。注意,这个问题也包括那些占据屏幕较大区域,且包括了手势交互区域的视图/控件。比如 DrawerLayout 或尺寸较大的 ViewPager。
这个紧接着问题 3 。在问题 3 中回答 "是" 的视图,是否需要用户在其上滑动或拖拽?
有不少用例会在本题回答 "是": 包括前面提到的进度条、底部弹出菜单 (Bottom Sheet) 或者可以通过滑动打开的弹出菜单 (PopupMenu)。
Bottom Sheet https://developer.android.google.cn/reference/com/google/android/material/bottomsheet/BottomSheetBehavior.html PopupMenu https://developer.android.google.cn/reference/androidx/appcompat/widget/PopupMenu
紧接着问题 4,进一步确认该视图是否完全或大部分位于手势交互区域内。
如果您的视图放置在一个可滚动操作的容器 (如 RecyclerView) 中,那么请这么理解这个问题: 该视图是否完全或大部分位于手势交互区域中?如果用户可以将视图滚动到手势交互区域之外,则应该视为没有交互冲突。
RecyclerView https://developer.android.google.cn/reference/androidx/recyclerview/widget/RecyclerView.html
对 Android 10 来说,强制交互区域只有一个,那就是屏幕底部。该区域内的滑动操作能让用户返回主屏或访问最近使用的其他应用。这个强制交互区域可能会在将来的平台版本中发生变化,但现在我们只需要考虑屏幕底部即可。
非模态的底部弹出菜单,因为这种菜单常常会在屏幕底部折叠为一个较小的视图,而且还需要滑动操作。
屏幕底部的水平页面切换,例如软键盘里选择不同表情包的 UI。
OK,现在我已经解释了流程图中的问题,下面我们来详细说说流程图中给出的解决方案。
解决方案 1: 无需处理手势冲突
最简单的 "解决方案" ,只需要……什么都不做!
当然,也许您还可以 (参考接下来的几种解决方案) 做点优化,但在启用了手势导航的应用中,您应该不会遇到大问题。
如果流程图为您选择了 "什么都不做" 的答案,但您依然觉得应用的使用有问题,请务必反馈给我们。
向我们提交反馈
http://issuetracker.google.com/
解决方案 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
}
Jetpack Core Library v1.2.0 https://developer.android.google.cn/jetpack/androidx/releases/core 系统交互热区 API https://developer.android.google.cn/reference/androidx/core/view/WindowInsetsCompat.html#getSystemGestureInsets()
衍生阅读: 如何让 WindowInsets 更易于使用 https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
△ 将进度条移到视图的顶部
解决方案 3: 使用手势区域排除 API
View.setSystemGestureExclusionRects() http://developer.android.google.cn/reference/android/view/View.html#setSystemGestureExclusionRects(java.util.List%3Candroid.graphics.Rect%3E) Window.setSystemGestureExclusionRects() http://developer.android.google.cn/reference/android/view/Window.html#setSystemGestureExclusionRects(java.util.List%3Candroid.graphics.Rect%3E) Android View https://developer.android.google.cn/reference/android/view/View.html
在上图中,由于进度条的播放头正好位于右侧手势区内,因此系统认为用户正在用手势执行 "返回" 操作,因此显示了 "向后" 的箭头。这时就会让用户感到困惑,因为他们可能并不想后退。出现这种冲突时,我们就可以使用上面提到的手势区域排除 API 来解决。
手势区域排除 API 通常会在两个地方被调用: 当视图被布局时 (onLayout),或是当视图被绘制时 (onDraw)。您的视图会传入一个 List<Rect>,其中包含应该切出 (即不响应系统手势) 的矩形区域。如前所述,这些矩形须位于视图自己的坐标系中。
通常,您会创建一个类似于下面的方法,该方法会在 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
}
上例的完整代码 https://gist.github.com/chrisbanes/4e613a76ebc6a2d3e64d3eee911e1e7d
做完这个 "切出" 操作后,在屏幕边缘附近进行快进/快退操作就没有问题了:
注意: SeekBar 实际上会在 Android 10 中自动为您执行上述切出操作,因此您无需在 Seekbar 中这么做。这里只是作为示例向您展示处理冲突的做法。
限制条件
为什么要有限制?
为什么是 200dp?
如果开发者要求在边缘上切出 200dp 以上的区域会怎样?
△ 开发者请求切出 50 + 50 + 125 + 50 dp 的区域,但系统只兑现最下面的总计 200dp
不会,系统仅计算屏幕范围内的切出矩形。同样,如果视图只有一部分显示在屏幕内,则仅计算所请求矩形的屏幕内可见部分。
请关注下一篇连载
想了解更多 Android 内容?
在公众号首页发送关键词 "Android",获取相关历史技术文章;
在公众号首页发送关键词 "ADS",获取开发者峰会演讲中文字幕视频;
还有更多疑惑?欢迎点击菜单 "联系我们" 反馈您在开发过程中遇到的问题。
推荐阅读