13.1 精通自定义 View 之控件高级属性——GestureDetector 手势检测

返回自定义 View 目录

13.1.1 概述

GestureDetector,手势检测类,通过这个类可以识别很多手势。在识别出手势之后,具体的事物处理则交由程序员自己来实现。

此类提供了两个接口(OnGestureListener、OnDoubleTapListener)和一个外部类(SimpleOnGestureListener)。这个外部类其实是两个接口中所有函数的集成,它包含了这两个接口里所有必须实现的函数,而且都已经被重写,但所有函数体都是空的。该类是一个静态类,程序员可以在外部继承这个类,重写里面的手势处理函数。

13.1.2 GestureDetector.OnGestureListener 接口

1. 基本讲解

如果我们写一个类并继承自 OnGestureListener,则会提示有几个必须重写的函数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private class GestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}

这些函数在什么情况下才会被触发呢?

  • onDown(MotionEvent e):用户按下屏幕就会触发该函数。
  • onShowPress(MotionEvent e):如果按下的时间超过瞬间,而且在按下的时候没有松开或者是拖动的,该函数就会被触发。
  • onLongPress(MotionEvent e):长按触摸屏,超过一定时长,就会触发这个函数。

触发顺序:

1
onDown —> onShowPress —> onLongPress

  • onSingleTapUp(MotionEvent e):一次单独的轻击抬起操作,也就是轻击一下屏幕,立刻抬起来,才会触发这个函数。当然,如果除 down 以外还有其他操作,就不再算是单独操作了,也就不会触发这个函数。

单击一下非常快的(不滑动)Touchup,触发顺序:

1
onDown —> onSingleTapUp —> onSingleTapConfirmed

单击一下稍微慢一点的(不滑动)Touchup,触发顺序:

1
onDown —> onShowPress —> onSingleTapUp —> onSingleTapConfirmed

  • onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY):滑屏,用户按下触摸屏、快速移动后松开,由一个 MotionEvent ACTION_DOWN、多个 ACTION_MOVE、一个 ACTION_UP 触发。
  • onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY):在屏幕上拖动事件。无论是用手拖动 View,还是以抛的动作滚动,都会多次触发这个函数,在 ACTION_MOVE 动作发生时就会触发该函数。

滑屏,即手指出动屏幕后,稍微滑动后立即松开,触发顺序为:

1
onDown —> onScroll —> onScroll —> onScroll —> ... —> onFling

拖动,触发顺序为:

1
onDown —> onScroll —> onScroll —> onFling

可见,无论是滑屏还是拖动,影响的只是中间 onScroll 被触发的数量而已,最终都会触发 onFling 事件。

2. 示例

要使用 GestureDetector,有四步要走。
1)创建 OnGestureListener() 监听函数。

1
2
3
4
5
6
// 可以构造实例
GestureDetector.OnGestureListener listener = new GestureDetector.OnGestureListener() {};
// 也可以构造类
private class GestureListener implements GestureDetector.OnGestureListener {
}

2)创建 GestureDetector 实例 mGestureDetector。
构造函数有以下几个,根据需要选择即可。

1
2
3
4
GestureDetector(OnGestureListener listener)
GestureDetector(Context context, OnGestureListener listener)
GestureDetector(Context context, OnGestureListener listener, Handler handler)
GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean unused)

3)在 onTouch(View v, MotionEvent event) 中进行拦截。

1
2
3
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}

4)绑定控件。

1
2
TextView tv = findViewById(R.id.tv);
tv.setOnTouchListener(this);

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private GestureDetector mGestureDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
mGestureDetector = new GestureDetector(new GestureListener());
TextView tv = findViewById(R.id.tv);
tv.setOnTouchListener(this);
tv.setFocusable(true);
tv.setClickable(true);
tv.setLongClickable(true);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class GestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
Log.i("MyGesture", "onDown");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.i("MyGesture", "onShowPress");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.i("MyGesture", "onSingleTapUp");
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.i("MyGesture", "onScroll");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("MyGesture", "onLongPress");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i("MyGesture", "onFling");
return false;
}
}
}

13.1.3 GestureDetector.OnDoubleTapListener 接口

1. 构建

有两种方式设置双击监听。
方法一:新建一个类,同时派生自 OnGestureListener 和 OnDoubleTapListener。

1
2
3
private class GestureListener implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
}

方法二:
使用 GestureDetector.setOnDoubleTapListener() 函数设置双击监听。

1
2
3
4
5
6
7
8
9
10
// 构建 GestureDetector 实例
mGestureDetector = new GestureDetector(new GestureListener());
private class GestureListener implements GestureDetector.OnGestureListener {
}
// 设置双击监听
mGestureDetector.setOnDoubleTapListener(new DoubleTapListener());
private class DoubleTapListener implements GestureDetector.OnDoubleTapListener {
}

无论方法一还是方法二,都需要派生自 GestureDetector.OnGestureListener。

2. 函数讲解

先来看下 OnDoubleTapListener 接口必须重写的三个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private class DoubleTapListener implements GestureDetector.OnDoubleTapListener {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
}

  • onSingleTapConfirmed(MotionEvent e):单击事件,用来判定该次单击是 SingleTap,而不是 DoubleTap。如果连续单击两次,就是 DoubleTap 手势;如果只单击一次,系统等待一段时间后没有收到第二次单击,则判定该次单击为 SingleTap,而不是 DoubleTap,然后触发 SingleTapConfirm 事件。触发顺序是:onDown —> onSingleTapUp —> onSingleTapConfirmed。有这样一个函数 onSingleTapUp(),它和 onSingleTapConfirmed() 函数容易混淆。二者的区别是:对于 onSingleTapUp() 函数来说,只要手抬起就会被触发;而对于 onSingleTapConfirmed() 函数来说,如果双击,则该函数就不会被触发。
  • onDoubleTap(MotionEvent e):双击事件。
  • onDoubleTapEvent(MotionEvent e):双击间隔中发生的动作。指在触发 onDoubleTap 以后,在双击之间发生的其他动作,包含 down、up 和 move 事件。

在 13.1.2 节例子的基础上,添加双击监听,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private GestureDetector mGestureDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
mGestureDetector = new GestureDetector(new GestureListener());
mGestureDetector.setOnDoubleTapListener(new DoubleTapListener());
TextView tv = findViewById(R.id.tv);
tv.setOnTouchListener(this);
tv.setFocusable(true);
tv.setClickable(true);
tv.setLongClickable(true);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class GestureListener implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
Log.i("MyGesture", "onDown");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.i("MyGesture", "onShowPress");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.i("MyGesture", "onSingleTapUp");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.i("MyGesture", "onScroll");
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("MyGesture", "onLongPress");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.i("MyGesture", "onFling");
return true;
}
}
private class DoubleTapListener implements GestureDetector.OnDoubleTapListener {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("MyGesture", "onSingleTapConfirmed");
Toast.makeText(MainActivity.this, "onSingleTapConfirmed", Toast.LENGTH_LONG).show();
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("MyGesture", "onDoubleTap");
Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_LONG).show();
return true;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.i("MyGesture", "onDoubleTapEvent:" + e.getAction());
Toast.makeText(MainActivity.this, "onDoubleTapEvent", Toast.LENGTH_LONG).show();
return true;
}
}
}

双击所对应的事件触发顺序如下图所示。

  • 第二次单击时,先触发 onDoubleTap,再触发 onDown。
  • 在触发 onDoubleTap 以后,就开始触发 onDoubleTapEvent。onDoubleTapEvent 后面的数字代表当前的事件,0 代表 ACTION_DOWN,1 代表 ACTION_UP,2 代表 ACTION_MOVE。

轻轻单击一下,对应的事件触发顺序如下:

13.1.4 GestureDetector.SimpleOnGestureListener 类

SimpleOnGestureListener 类与 OnGestureListener 和 OnDoubleTapListener 接口的不同之处在于:
1)这是一个类,在它的基础上新建类,要用 extends 派生,而不能用 implements 继承。
2)OnGestureListener 和 OnDoubleTapListener 接口里的函数都是被强制重写的,即使用不到也要重写出来一个空函数;而在 SimpleOnGestureListener 类的实例或派生类中不必如此,可以根据情况,用到哪个函数就重写哪个函数,因为 SimpleOnGestureListener 类本身已经实现了这两个接口中的所有函数,只是里面全是空的而已。

下面利用 SimpleOnGestureListener 类来重新实现前面的几个效果,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
private GestureDetector mGestureDetector;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.act_main);
mGestureDetector = new GestureDetector(new XSimpleGestureListener());
TextView tv = findViewById(R.id.tv);
tv.setOnTouchListener(this);
tv.setFocusable(true);
tv.setClickable(true);
tv.setLongClickable(true);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class XSimpleGestureListener extends GestureDetector.SimpleOnGestureListener {
/***** OnGestureListener 的函数 *****/
public boolean onDown(MotionEvent e) {
return false;
}
public void onShowPress(MotionEvent e) {
}
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
public void onLongPress(MotionEvent e) {
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
/***** OnDoubleTapListener 的函数 *****/
public boolean onDoubleTap(MotionEvent e) {
return false;
}
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
}
}

13.1.5 onFling() 函数的应用

可以利用 onFling() 函数来识别当前用户是在左滑还是在右滑。先来看一下 onFling() 函数的参数。

1
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)

参数:

  • e1:第一个 ACTION_DOWN MotionEvent。
  • e2:最后一个 ACTION_DOWN MotionEvent。
  • velocityX:X 轴上的移动速度,单位为像素/秒。
  • velocityY:Y 轴上的移动速度,单位为像素/秒。

实现的功能:当用户向左滑动距离超过 100 像素,且滑动速度超过 100 像素/秒时,即判断为向左滑动;向右同理。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class XSimpleGestureListener extends GestureDetector.SimpleOnGestureListener {
final int FLING_MIN_DISTANCE = 100;
final int FLING_MIN_VELOCITY = 100;
...
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// 向左滑
if (e1.getX() - e2.getX() > FLING_MIN_DISTANCE
&& Math.abs(velocityX) > FLING_MIN_VELOCITY) {
Log.i("MyGesture", "Fling left");
}
// 向右滑
else if (e2.getX() - e1.getX() > FLING_MIN_DISTANCE
&& Math.abs(velocityX) > FLING_MIN_VELOCITY) {
Log.i("MyGesture", "Fling right");
}
return true;
}
}