如今的 Android 手机已经离不开手指与屏幕的交互了,基本上只要在使用手机就避免不了手势的识别,相信各位学习 Android 开发的朋友们大都与手势交互打过交道,我的这篇博客就是使用 GestureDetector 实现手势识别。
1、MotionEvent
如今在国内,按键手机已经是少之又少了,至少我身边的人用的都是触屏的机器,这也就说明我们手机程序开发者开发的应用应该都是围绕着触控来识别操作。拿视屏播放器举例吧,无论是快进后退,还是调节音量,亮度都是根据触控的手势不同来处理得到不同的效果的。
当用户触摸屏幕的时候,就是三种动作:按下、移动、抬起,这样就可以产生各种各样的手势。
MotionEvent 也就是触摸事件,这个类封装了手势、触摸笔、轨迹球等动作事件,且内部封装用于记录横轴和纵轴坐标的属性 X 和 Y,通过方法可以从 MotionEvent 对象中获得触摸事件发生的坐标。
1、坐标方法
每个触摸事件都代表用户在屏幕上的一个动作,而每个动作必定有其发生的位置。在 MotionEvent 中就有一系列与标触摸事件发生位置相关的函数:
getX() 和 getY():由这两个函数获得的 x , y 值是相对的坐标值,相对于消费这个事件的视图的左上点的坐标。
getRawX() 和 getRawY():有这两个函数获得的 x , y 值是绝对坐标,是相对于屏幕的。
如图所示,相信大家都对各个获得坐标的方法的作用一目了然啦,这里说明一下,它们都是以像素为单位的。
2、事件类型
1、基本类型
MotionEvent 有个方法 getAction(),用它可以获得当前 MotionEvent 对象的事件类型。
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
事件类型就是指 MotionEvent 对象所代表的动作。比如说,当你的一个手指在屏幕上滑动一下时,系统会产生一系列的触摸事件对象,它们所代表的动作有所不同。这些事件的事件类型就分别为 ACTION_DOWN,ACTION_MOVE 和 ACTION_UP,分别对应着按下、移动、抬起三种动作。上述这个动作所产生的一系列事件,被称为一个事件流,它包括一个 ACTION_DOWN 事件,很多个 ACTION_MOVE 事件,和一个 ACTION_UP 事件。
2、ACTION_CANCEL
除了这三个类型外,还有很多不同的事件类型,比如 ACTION_CANCEL,它代表当前的手势被取消。要理解这个类型,就必须要了解 ViewGroup 分发事件的机制。
当 TouchEvent 发生时,首先 Activity 将 TouchEvent 传递给最顶层的 view,TouchEvent 最先到达最顶层 view 的 dispatchTouchEvent(),然后由 dispatchTouchEvent() 方法进行分发,如果 dispatchTouchEvent() 返回 true,则交给这个 view 的 onTouchEvent() 处理;如果返回 false,则交给这个 view 的 interceptTouchEvent() 方法来决定是否要拦截这个事件,如果返回 true,也就是拦截掉了,则交给它的 onTouchEvent() 来处理;如果返回 false ,那么就传递给子 view ,由子 view 的 dispatchTouchEvent() 再来开始这个事件的分发。如果事件传递到某一层的子 view 的 onTouchEvent() 上了,方法返回了 false,那么这个事件会从这个 view 往上传递,都是 onTouchEvent() 来接收。而如果传递到最上面的 onTouchEvent() 也返回 false 的话,这个事件就会“消失”,而且接收不到下一次事件。
一般来说,如果一个子视图接收了父视图分发给它的 ACTION_DOWN 事件,那么与 ACTION_DOWN 事件相关的事件流就都要分发给这个子视图,但是如果父视图希望拦截其中的一些事件,不再继续转发事件给这个子视图的话,那么就需要给子视图一个 ACTION_CANCEL 事件。
3、Pointer
上面所讲的都是一个手指在屏幕的触摸事件,所以如果是两个或者是多个手指同时触发事件时,产生的事件就要另外考虑啦。
为了可以表示多个触摸点的动作,MotionEvent 中引入了 Pointer 的概念,一个 pointer 就代表一个触摸点,每个 pointer 都有自己的事件类型,也有自己的横纵坐标值。一个 MotionEvent 对象中可能会存储多个 pointer 的相关信息,每个 pointer 都会有一个自己的 id 和 index。
pointer 的 id 在整个事件流中是不会发生变化的,但是 index 会发生变化。MotionEvent 类中的很多方法都是可以传入一个 int 值作为参数的,其实传入的就是 pointer 的 index 值。比如 getX(pointerIndex) 和getY(pointerIndex),此时,它们返回的就是 index 所代表的触摸点相关事件坐标值。
由于 pointer 的 index 值在不同的 MotionEvent 对象中会发生变化,但是 id 值却不会变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其 id,使用 findPointerIndex(int) 来获得其index值,然后再获得其他信息。
根据 Pointer 的概念,MotionEvent 引入了两个事件类型:
- ACTION_POINTER_DOWN:代表用户又使用一个手指触摸到屏幕上,也就是说,在已经有一个触摸点的情况下,又新出现了一个触摸点。
- ACTION_POINTER_UP:代表用户的一个手指离开了触摸屏,但是还有其他手指还在触摸屏上。也就是说,在多个触摸点存在的情况下,其中一个触摸点消失了。它与 ACTION_UP 的区别就是,它是在多个触摸点中的一个触摸点消失时(此时,还有触摸点存在,也就是说用户还有手指触摸屏幕)产生,而 ACTION_UP 可以说是最后一个触摸点消失时产生。
3、动作常量
常见的动作常量:
public static final int ACTION_DOWN = 0;单点触摸动作
public static final int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE = 2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
public static final int ACTION_OUTSIDE = 4;触摸动作超出边界
public static final int ACTION_POINTER_DOWN = 5;多点触摸动作
public static final int ACTION_POINTER_UP = 6;多点离开动作
以下是一些非touch事件:
public static final int ACTION_HOVER_MOVE = 7;
public static final int ACTION_SCROLL = 8;
public static final int ACTION_HOVER_ENTER = 9;
public static final int ACTION_HOVER_EXIT = 10;
掩码常量:
ACTION_MASK = 0X000000ff 动作掩码
ACTION_POINTER_INDEX_MASK = 0X0000ff00 触摸点索引掩码
ACTION_POINTER_INDEX_SHIFT = 8 获取触摸点索引需要移动的位数
4、getAction
获得手势的方法有 getAction(),getActionMasked(),getActionIndex(),那么这几个有什么不同呢?
从上面讲过的,一个 MotionEvent 对象中可以包含多个触摸点的事件。当 MotionEvent 对象只包含一个触摸点的事件时,getAction() 和 getActionMasked() 的结果是相同的,但是当包含多个触摸点时,结果就不同啦。
getAction() 获得的 int 值是由 pointer 的 index 值和事件类型值组合而成的,而 getActionMasked() 则只返回事件的类型值,用于多指触控。
getAction() returns 0x0105.
getActionMasked() will return 0x0005
其中0x0100就是pointer的index值。
getActionIndex() 返回 ACTION_POINTER_DOWN,ACTION_POINTER_UP 相关的下标,这个下标可能会在 getPointerId(),getX(),getY(),getPressed(),getSize() 中用到来获取手指的关于按下或者抬起的信息。
一般来说,getAction() & ACTION_POINTER_INDEX_MASK 就获得了pointer的id,等同于 getActionIndex();getAction() & ACTION_MASK 就获得了 pointer 的事件类型,等同于getActionMasked()。
5、多指触控方法
getPointerCount():返回 MotionEvent 中表示了多少手指数
getPointerId(int pointerIndex): 返回指针索引关联的指针ID,在手指按下和抬起之间ID始终不变。
getY(int pointerIndex):返回指定指针索引的当前的Y坐标位置
getX(int pointerIndex):返回指定指针索引的当前的X坐标位置
getHistorySize():返回某跟手指触摸事件的历史位置的记录数
getHistoricalX (int pointerIndex, int pos):返回指定指针索引的手指上一次的X坐标位置,只针对移动事件。参数pos是指第几个旧位置,这个值不能超过getHistorySize()返回的值
getHistoricalY (int pointerIndex, int pos):返回指定指针索引的手指上一次的Y坐标位置,只针对移动事件。参数pos是指第几个旧位置,这个值不能超过getHistorySize()返回的值
findPointerIndex(int pointerId) 通过 PointerId 获取到当前状态下PointIndex,之后通过PointIndex获取其他内容
2、GestureDetector
在 View 类有个 OnTouchListener内部接口,通过重写它的 onTouch(View v, MotionEvent event) 方法,我们可以处理一些 Touch 事件,但是这个方法太过简单,如果需要处理一些复杂的手势,用这个接口就会很麻烦(因为我们要自己根据用户触摸的轨迹去判断是什么手势)。
Android sdk给我们提供了GestureDetector(手势识别)类,通过这个类我们可以识别很多的手势,主要是通过它的 onTouchEvent() 方法完成了不同手势的识别。虽然它能识别手势,但是不同的手势要怎么处理,应该是提供给程序员实现的。
使用 GestureDetector 实现手势的识别可以简单分为四个过程:
- 触屏的一刹那,开始触发 MotionEvent 事件
- 事件被 OnTouchListener 监听,在 onTouch() 方法中获得 MotionEvent 对象
- GestureDetector 转发 MotionEvent 对象至 OnGestureListener 监听器
- OnGestrueListener 获得该对象后,根据该对象封装的信息作出合适的反馈
GestureDetector 这个类对外提供了两个接口:OnGestureListener,OnDoubleTapListener,还有一个内部类 SimpleOnGestureListener。
就像 Button,有单击和双击,我们在触屏的时候也会使用,GesturDetector 也提供了接口给我们使用,我们就可以实现在全屏幕上都有单击和双击的效果。
OnGestureListener接口:用来通知普通的手势单击事件,该接口有如下六个回调函数:
- onDown(MotionEvent e):down 事件;
- onSingleTapUp(MotionEvent e):一次点击 up 事件;在 touch down 后既没有滑动(onScroll),也没有长按(onLongPress),然后 touch up 时触发。
- onShowPress(MotionEvent e):down 事件发生而 move 或者 up 还没发生前触发该事件,也就是短按事件。
- onLongPress(MotionEvent e):长按事件;Touch了不移动一直到Touch down时触发 。
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY):滑动手势事件;Touch了滑动一点距离后,在 ACTION_UP 时才会触发。
参数:e1 是第1个 ACTION_DOWN MotionEvent 并且只有一个;e2 是最后一个ACTION_MOVE MotionEvent;velocityX 为X轴上的移动速度,像素/秒 ;velocityY 为Y轴上的移动速度,像素/秒。
触发条件:X轴的坐标位移大于 FLING_MIN_DISTANCE,且移动速度大于 FLING_MIN_VELOCITY 个像素/秒就可触发滑动事件。
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY):在屏幕上的拖动事件。无论是用手拖动 view,或者是以抛的动作滚动,都会多次触发,这个方法在 ACTION_MOVE 动作发生时就会触发。
抛:手指触动屏幕后,稍微滑动后立即松开。
OnDoubleTapListener 接口:用来通知 DoubleTap 事件,类似于鼠标的双击事件。
- onDoubleTap(MotionEvent e):在双击的第二下,Touch down时触发 。
- onDoubleTapEvent(MotionEvent e):通知 DoubleTap 手势中的事件,包含 down、up 和 move 事件(这里指的是在双击之间发生的事件,例如在同一个地方双击会产生 DoubleTap 手势,而在 DoubleTap 手势里面还会发生 down 和 up 事件,这两个事件由该函数通知);双击的第二下 Touch down 和 up 都会触发,可用 e.getAction() 区分。
- onSingleTapConfirmed(MotionEvent e):用来判定该次点击是 SingleTap 而不是 DoubleTap,如果连续点击两次就是 DoubleTap 手势,如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为 SingleTap 而不是 DoubleTap,然后触发 SingleTapConfirmed()。这个方法不同于 onSingleTapUp(),它是在 GestureDetector 确信用户在第一次触摸屏幕后,没有紧跟着第二次触摸屏幕,也就是不是“双击”的时候触发 。
点击一下非常快的(单击)Touch up:
onDown() -> onSingleTapUp() -> onSingleTapConfirmed()
点击一下稍微慢点的(短按)Touch up:
onDown() -> onShowPress() -> onSingleTapUp() -> onSingleTapConfirmed()
点击一下长时间的(长按)Touch up:
onDown() -> onShowPress() -> onLongPress()
SimpleOnGestureListener类是 GestureDetector 提供给我们的一个更方便的响应不同手势的类,这个类实现了上述两个接口(但是所有的方法体都是空的),该类是static class,也就是说它实际上是一个外部类。程序员可以在外部继承这个类,重写里面的手势处理方法。
3、使用GestureDetector
这里就写个使用 GestureDetector 重写滑动的小例子来看看一般是如何使用 GestureDetector 的。
public class MainActivity extends AppCompatActivity {
private ImageView mImageView;
private GestureDetector mGestureDetector;
private class myGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (e1.getX() - e2.getX() > 50) {
Toast.makeText(MainActivity.this, "向左滑动", Toast.LENGTH_SHORT).show();
} else if (e2.getX() - e1.getX() > 50) {
Toast.makeText(MainActivity.this, "向右滑动", Toast.LENGTH_SHORT).show();
}
return super.onFling(e1, e2, velocityX, velocityY);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImageView = (ImageView) findViewById(R.id.id_iv);
mGestureDetector = new GestureDetector(this, new myGestureListener());
mImageView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mGestureDetector.onTouchEvent(event);
return true;
}
});
}
}
因为我们这里是要对滑动这个比单纯的按下和抬起要复杂的手势,所以在 mImageView 的 onTouchListener 监听器的 onTouch() 方法里我们要用 GestureDetector 来处理 MotionEvent,这会简单的多。
根据我上面对使用 GestureDetector 的步骤分析,我们要创建一个 GestureDetector 对象来转发 MotionEvent 对象,在实例化的时候我们就要使用 OnGestureListener,上面说过这是一个接口,Android 提供了实现了它的 SimpleOnGestureListener 给我们使用,我们写个类继承它即可。
这样我们就可以根据需要来对各个方法进行重写,这里我们就可以操控滑动时的状况。
结束语:本文仅用来学习记录,参考查阅。