安卓自定义 View 进阶:Android 手势检测(GestureDetector)

Android 手势检测,主要是 GestureDetector 相关内容的用法和注意事项,本文依旧属于事件处理这一体系,部分内容会涉及到之前文章提及过的知识点,如果你没看过之前的文章,可以到 自定义 View 系列 来查看这些内容。

在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。就拿最简单的双击来说吧,假如我们需要判断一个控件是否被双击(即在较短的时间内快速的点击两次),似乎是一个很容易的任务,但仔细考虑起来,要处理的细节问题也有不少,例如:

  1. 记录点击次数,为了判断是否被点击超过 1 次,所以必须记录点击次数。
  2. 记录点击时间,由于双击事件是较快速的点击两次,像点击一次后,过来几分钟再点击一次肯定不能算是双击事件,所以在记录点击次数的同时也要记录上一次的点击时间,我们可以设置本次点击距离上一次时间超过一定时间(例如:超过100ms)就不识别为双击事件。
  3. 点击状态重置,在响应双击事件,或者判断不是双击事件的时候要重置计数器和上一次点击时间。重置既可以在点击的时候判断并进行重新设置,也可以使用定时器等超过一定时间后重置状态。

这样看起来,判断一个双击事件就有这么多麻烦事情,更别其他的手势了,虽然这些看起来都很简单,但设计起来需要考虑的细节情况实在是太多了。

那么有没有一种更好的方法来方便的检测手势呢?当然有啦,因为这些手势很常用,系统早就封装了一些方法给我们用,接下来我们就看看它们是如何使用的。

GestureDetector

GestureDetector 可以使用 MotionEvents 检测各种手势和事件。GestureDetector.OnGestureListener 是一个回调方法,在发生特定的事件时会调用 Listener 中对应的方法回调。这个类只能用于检测触摸事件的 MotionEvent,不能用于轨迹球事件。
(话说轨迹球已经消失多长时间了,估计很多人都没见过轨迹球这种东西)。

如何使用:

  • 创建一个 GestureDetector 实例。

  • 在onTouchEvent(MotionEvent)方法中,确保调用 GestureDetector 实例的 onTouchEvent(MotionEvent)。回调中定义的方法将在事件发生时执行。

  • 如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector OnGenericMotionEvent(MotionEvent)。

    GestureDetector 本身的方法比较少,使用起来也非常简单,下面让我们先看一下它的简单使用示例,分解开来大概需要三个步骤。

  • // 1.创建一个监听回调
  • SimpleOnGestureListener listener = new SimpleOnGestureListener() {
  • @Override public boolean onDoubleTap(MotionEvent e) {
  • Toast.makeText(MainActivity.this, "双击666", Toast.LENGTH_SHORT).show();
  • return super.onDoubleTap(e);
  • }
  • };
  • // 2.创建一个检测器
  • final GestureDetector detector = new GestureDetector(this, listener);
  • // 3.给监听器设置数据源
  • view.setOnTouchListener(new View.OnTouchListener() {
  • @Override public boolean onTouch(View v, MotionEvent event) {
  • return detector.onTouchEvent(event);
  • }
  • });

接下来我们先了解一下 GestureDetector 里面都有哪些内容。

1. 构造函数

GestureDetector 一共有 5 种构造函数,但有 2 种被废弃了,1 种是重复的,所以我们只需要关注其中的 2 种构造函数即可,如下:

| 构造函数 |
| ---------------------------------------- |
| GestureDetector(Context context, GestureDetector.OnGestureListener listener) |
| GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler) |

第 1 种构造函数里面需要传递两个参数,上下文(Context) 和 手势监听器(OnGestureListener),这个很容易理解,就不再过多叙述,上面的例子中使用的就是这一种。

第 2 种构造函数则需要多传递一个 Handler 作为参数,这个有什么作用呢?其实作用也非常简单,这个 Handler 主要是为了给 GestureDetector 提供一个 Looper。

在通常情况下是不需这个 Handler 的,因为它会在内部自动创建一个 Handler 用于处理数据,如果你在主线程中创建 GestureDetector,那么它内部创建的 Handler 会自动获得主线程的 Looper,然而如果你在一个没有创建 Looper 的子线程中创建 GestureDetector 则需要传递一个带有 Looper 的 Handler 给它,否则就会因为无法获取到 Looper 导致创建失败。

第 2 种构造函数使用方式如下(下面是两种在子线程中创建 GestureDetector 的方法):

  • // 方式一、在主线程创建 Handler
  • final Handler handler = new Handler();
  • new Thread(new Runnable() {
  • @Override public void run() {
  • final GestureDetector detector = new GestureDetector(MainActivity.this, new
  • GestureDetector.SimpleOnGestureListener() , handler);
  • // ... 省略其它代码 ...
  • }
  • }).start();
  • // 方式二、在子线程创建 Handler,并且指定 Looper
  • new Thread(new Runnable() {
  • @Override public void run() {
  • final Handler handler = new Handler(Looper.getMainLooper());
  • final GestureDetector detector = new GestureDetector(MainActivity.this, new
  • GestureDetector.SimpleOnGestureListener() , handler);
  • // ... 省略其它代码 ...
  • }
  • }).start();

当然了,使用其它创建 Handler 的方式也是可以的,重点传递的 Handler 一定要有 Looper,敲黑板,重点是 Handler 中的 Looper。假如子线程准备了 Looper 那么可以直接使用第 1 种构造函数进行创建,如下:

  • new Thread(new Runnable() {
  • @Override public void run() {
  • Looper.prepare(); // <- 重点在这里
  • final GestureDetector detector = new GestureDetector(MainActivity.this, new
  • GestureDetector.SimpleOnGestureListener());
  • // ... 省略其它代码 ...
  • }
  • }).start();

2.手势监听器

既然是手势检测,自然要在对应的手势出现的时候通知调用者,最合适的自然是事件监听器模式。目前 GestureDetecotr 有四种监听器。

监听器 简介
OnContextClickListener 这个很容易让人联想到ContextMenu,然而它和ContextMenu并没有什么关系,它是在Android6.0(API 23)才添加的一个选项,是用于检测外部设备上的按钮是否按下的,例如蓝牙触控笔上的按钮,一般情况下,忽略即可。
OnDoubleTapListener 双击事件,有三个回调类型:双击(DoubleTap)、单击确认(SingleTapConfirmed) 和 双击事件回调(DoubleTapEvent)
OnGestureListener 手势检测,主要有以下类型事件:按下(Down)、 一扔(Fling)、长按(LongPress)、滚动(Scroll)、触摸反馈(ShowPress) 和 单击抬起(SingleTapUp)
SimpleOnGestureListener 这个是上述三个接口的空实现,一般情况下使用这个比较多,也比较方便。

2.1 OnContextClickListener

由于 OnContextClickListener 主要是用于检测外部设备按钮的,关于它需要注意一点,如果侦听 onContextClick(MotionEvent),则必须在 View 的 onGenericMotionEvent(MotionEvent)中调用 GestureDetector 的 OnGenericMotionEvent(MotionEvent)。

由于目前我们用到这个监听器的场景并不多,所以也就不展开介绍了,重点关注后面几个监听器。

2.2 OnDoubleTapListener

这个很明显就是用于检测双击事件的,它有三个回调接口,分别是 onDoubleTap、onDoubleTapEvent 和 onSingleTapConfirmed。

2.2.1 onDoubleTap 与 onSingleTapConfirmed

如果你只想监听双击事件,那么只用关注 onDoubleTap 就行了,如果你同时要监听单击事件则需要关注 onSingleTapConfirmed 这个回调函数

有人可能会有疑问,监听单击事件为什么要使用 onSingleTapConfirmed,使用 OnClickListener 不行吗?从理论上是可行的,但是我并不推荐这样使用,主要有两个原因:
1.它们两个是存在一定冲突的,如果你看过 事件分发机制详解 就会知道,如果想要两者同时被触发,则 setOnTouchListener 不能消费事件,如果 onTouchListener 消费了事件,就可能导致 OnClick 无法正常触发。
2.需要同时监听单击和双击,则说明单击和双击后响应逻辑不同,然而使用 OnClickListener 会在双击事件发生时触发两次,这显然不是我们想要的结果。而使用 onSingleTapConfirmed 就不用考虑那么多了,你完全可以把它当成单击事件来看待,而且在双击事件发生时,onSingleTapConfirmed 不会被调用,这样就不会引发冲突。

如果你需要同时监听两种点击事件可以这样写:

  • GestureDetector detector = new GestureDetector(this, new GestureDetector
  • .SimpleOnGestureListener() {
  • @Override public boolean onSingleTapConfirmed(MotionEvent e) {
  • Toast.makeText(MainActivity.this, "单击", Toast.LENGTH_SHORT).show();
  • return false;
  • }
  • @Override public boolean onDoubleTap(MotionEvent e) {
  • Toast.makeText(MainActivity.this, "双击", Toast.LENGTH_SHORT).show();
  • return false;
  • }
  • });

关于 onSingleTapConfirmed 原理也非常简单,这一个回调函数在单击事件发生后300ms后触发(注意,不是立即触发的),只有在确定不会有后续的事件后,既当前事件肯定是单击事件才触发 onSingleTapConfirmed,所以在进行点击操作时,onDoubleTap 和 onSingleTapConfirmed 只会有一个被触发,也就不存在冲突了。

当然,如果你对事件分发机制非常了解的话,随便怎么用都行,条条大路通罗马,我这里只是推荐一种最简单而且不容易出错的实现方案。

2.2.2 onDoubleTapEvent

有些细心的小伙伴可能注意到还有一个 onDoubleTapEvent 回调函数,它是干什么的呢?它在双击事件确定发生时会对第二次按下产生的 MotionEvent 信息进行回调。

至于为什么要存在这样的回调,就要涉及到另一个比较细致的问题了,那就是 onDoubleTap 的触发时间,如果你在这些函数被调用时打印一条日志,那么你会看到这样的信息:

  • GCS-LOG: onDoubleTap
  • GCS-LOG: onDoubleTapEvent - down
  • GCS-LOG: onDoubleTapEvent - move
  • GCS-LOG: onDoubleTapEvent - move
  • GCS-LOG: onDoubleTapEvent - up

通过观察这些信息你会发现它们的调用顺序非常有趣,首先是 onDoubleTap 被触发,之后依次触发 onDoubleTapEvent 的 down、move、up 等信息,为什么说它们有趣呢?是因为这样的调用顺序会引发两种猜想,第一种猜想是 onDoubleTap 是在第二次手指抬起(up)后触发的,而 onDoubleTapEvent 是一种延时回调。第二种猜想则是 onDoubleTap 在第二次手指按下(dowm)时触发,onDoubleTapEvent 是一种实时回调。

通过测试和观察源码发现第二种猜想是正确的,因为第二次按下手指时,即便不抬起也会触发 onDoubleTap 和 onDoubleTapEvent 的 down,而且源码中逻辑也表明 onDoubleTapEvent 是一种实时回调。

这就引发了另一个问题,双击的触发时间,虽然这是一个细微到很难让人注意到的问题,假如说我们想要在第二次按下抬起后才判定这是一个双击操作,触发后续的内容,则不能使用 onDoubleTap 了,需要使用 onDoubleTapEvent 来进行更细微的控制,如下:

  • final GestureDetector detector = new GestureDetector(MainActivity.this, new GestureDetector.SimpleOnGestureListener() {
  • @Override public boolean onDoubleTap(MotionEvent e) {
  • Logger.e("第二次按下时触发");
  • return super.onDoubleTap(e);
  • }
  • @Override public boolean onDoubleTapEvent(MotionEvent e) {
  • switch (e.getActionMasked()) {
  • case MotionEvent.ACTION_UP:
  • Logger.e("第二次抬起时触发");
  • break;
  • }
  • return super.onDoubleTapEvent(e);
  • }
  • });

如果你不需要控制这么细微的话,忽略即可(Logger 是我自己封装的日志库,忽略即可)。

top Created with Sketch.