现在的移动设备(手机、pad等)一般都支持多点触摸,有些设备支持十点触摸,有些支持五点触摸,有些可能只支持两点触摸。在Android平台上,用户对屏幕的操作(点按、缩放、拖动等)统称为Gesture(手势),在此我们就用“手势操作”来统称用户的这些操作。
函数实现
在程序中,如果某一控件需要响应用户手势操作、进行进一步处理时,有如下两种方法:
- 创建新类继承自
android.view.View
,覆盖父类方法public boolean onTouchEvent (MotionEvent event)
。 - 在处理这些触摸操作的类中实现接口
android.view.View.OnTouchListener
,该接口只有一个方法public abstract boolean onTouch (View v, MotionEvent event)
,并调用View
的public void setOnTouchListener (View.OnTouchListener l)
方法对touchListener
进行设置。
MotionEvent
简介
用户操作信息全部在MotionEvent
内,MotionEvent
包含了以下信息:
- 当前操作的点数
- 每个点对应的坐标
- 每个点对应的手势动作
手势动作如下:
ACTION_DOWN
用户按下了某一点ACTION_MOVE
用户按住一点在屏幕上移动ACTION_UP
用户从屏幕上移开某一点对应的手指ACTION_CANCEL
当前的手势操作被取消(在稍后我会详细解释此动作)ACTION_OUTSIDE
手势操作发生在UI组件外ACTION_HOVER_ENTER
当前点没有按下,但是进入了window
或view
的边界之内ACTION_HOVER_EXIT
当前点没有按下,但是已经移动至window
或view
的边界之外ACTION_HOVER_MOVE
当前点没有按下,当时点的位置发生了变化
单点手势操作
我们由浅入深,先看看单点手势操作。请参考HelloEventSingglePoint项目。
在该项目中,我们创建了一个自定义View
:EventView
,在EventView
中实现了方法public boolean onTouchEvent (MotionEvent event)
运行程序后,用单个手指在屏幕上进行触摸操作,会输出日志:
- Action is ACTION_DOWN
- Action is ACTION_MOVE
- Action is ACTION_MOVE… (若干)
- Action is ACTION_UP
多点手势操作
我解释一下Android里面手势操作的基本理念,为了简单起见,以两个点A、B为例,那么下面是一个手势操作列表:
A
点按下,产生ACTION_DOWN
动作B
点按下,此时A
点对应的动作是ACTION_MOVE
,B
点对应的动作为ACTION_POINTER_DOWN
A
、B
在屏幕上移动,产生动作为A
:ACTION_MOVE
;B
;ACTION_MOVE
A
或者B
抬起,产生动作为A
(B
):ACTION_POINTER_UP
;B
(A
):ACTION_MOVE
- 剩下的一个点抬起,产生动作
ACTION_UP
也就是说,在每一个MotionEvent
事件中,最多只有一个点会产生DOWN
或者UP
,其他点(如果有的话)为ACTION_MOVE
,读者不仅会问,为什么不存在两个点同时发出DWON
或者UP
的情况呢?这一点很好解释,一般来说人的动作就算是再训练有素,两个手指头按下的时间也不可能完全同步。退一步,就算两个手指头同时按下了,在系统处理层面,为了简化起见,系统也可以把同时按下的事件分解成DOWN
、DOWN
+ACTION_MOVE
两个事件传给应用程序。推而广之,更多点手势操作也是这个道理。
在多点手势操作过程中,最先发生的是ACTION_DOWN
,之后其他点的按下、抬起产生的动作为ACTION_POINTER_DOWN
、ACTION_POINTER_UP
,最后一个点抬起会产生ACTION_UP
。对于ACTION_DOWN
、ACTION_UP
之间的其他点,Android
称之为maskedAction
,可以使用函数public final int getActionMasked ()
来查询这个动作是ACTION_POINTER_DOWN
还是ACTION_POINTER_UP
,如果getActionMasked()
返回了ACTION_MOVE
,则表明当前用户正在使用若干(一个或者多个)手指在屏幕上移动,没有手指按下或抬起。函数public final int getActionIndex ()
用来获取当前按下/抬起的点的标识。如果当前没有任何点抬起/按下,该函数返回0
。
跟单点手势操作一样,多点手势操作的信息也是包含在方法onTouchEvent
的MotionEvent
参数内,MotionEvent
中有如下函数可以获取多点触摸信息:
int getPointerCount()
手势操作所包含的点的个数int getPointerId(int pointerIndex)
返回点的唯一标识(在多点手势操作中,每个手指对应点的index是不固定的,也就是说食指、中指在屏幕上移动时,某一个事件食指对应点的index为0、中指为1,而在下一个事件中可能就会变为食指对应的index为1、中指为0。所以引入了一个pointer identifier
的概念,既每个点都会有一个标识,在整个手势操作中,某根手指头对应的点的标识是保证不会改变的)float getX(int pointerIndex)
返回手势操作点的x坐标float getY(int pointerIndex)
返回手势操作点的y坐标public final int getActionMasked ()
获取特殊点的
请参考项目HelloEventMultiPoint
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
//io.github.tianshanxuester.helloeventmultipoint
@Override
public boolean onTouchEvent (MotionEvent event) {
Log.i("HelloEventMultiPoint", "Total pointer count is " + event.getPointerCount());
String actionString = actionToString(event.getAction());
Log.i("HelloEventMultiPoint", "Main action is " + actionString);
int maskedAction = event.getActionMasked();
int pointId = event.getActionIndex();
Log.i("HelloEventMultiPoint", "Masked action is " + actionToString(maskedAction) + "\tpointId is " + pointId);
Log.i("HelloEventMultiPoint", "====================");
return true;
}
private String actionToString(int action) {
String actionString = null;
switch(action) {
case MotionEvent.ACTION_CANCEL:
actionString = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_DOWN:
actionString = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
actionString = "ACTION_MOVE";
break;
case MotionEvent.ACTION_OUTSIDE:
actionString = "ACTION_OUTSIDE";
break;
case MotionEvent.ACTION_UP:
actionString = "ACTION_UP";
break;
case MotionEvent.ACTION_POINTER_DOWN:
actionString = "ACTION_POINTER_DOWN";
break;
case MotionEvent.ACTION_POINTER_UP:
actionString = "ACITON_POINTER_UP";
break;
default:
actionString = "" + action;
}
return actionString;
}
触发ACTION_CANCEL
事件
运行上面两个例子,如果没什么差错的话,你是不会看到ACTION_CANCEL
事件的,为什么呢?
要触发ACTION_CANCEL
,就先得了解一个类ViewGroup
,ViewGroup
是一个放置其他views
(子view)的特殊view
,它是布局类(*Layout)、视图容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基类。
也就是说ViewGroup
一般是做为父视图来容纳、管理其他子视图的。既然管理,在用户手势操作过程中,就会存在父视图不希望子视图响应用户手势操作的情况。Android
提供了一个函数public boolean onInterceptTouchEvent (MotionEvent ev)
,在用户手势操作时,系统先调用父视图(一个继承自ViewGroup
的类)的这个函数,来决定当前手势操作是由父视图还是子视图来响应、处理。我们仔细看看这个函数名,函数名中有一个单词intercept
,经过查词典,这个单词的中文意思是拦截
。在用户的一个完整手势操作过程中(起自ACTION_DOWN
,终于ACTION_UP
),对于每一次的MotionEvent
Android
都会调用该函数,向父视图查询是否拦截当前MotionEvent
,如果父视图返回false
:不拦截,则系统会调用子视图的onTouchEvent
函数;如果父视图返回true
:拦截,则系统调用父视图的onTouchEvent
。等等,有人不禁要问了,如果在这个完整手势操作过程中,父视图初期返回false
、后期返回true
会是一个什么样的情况呢(捣乱的来了)?这个嘛,是这个样子的,一开始返回false
,毫无疑问,子视图会被调用onTouchEvent
,但凡父视图在函数onInterceptTouch
中有一次返回了true
,那这一完整手势操作内所有后续的MotionEvent
都会调用父视图的onTouchEvent
,即使父视图后期反悔而改成返回false
也不行(没有后悔药)。在这种父视图先返回false
,后返回true
的情况下,子视图收不到后续的事件,而只是在父视图由返回false
改成返回true
(拦截)的时候收到ACTION_CANCEL
事件。 见项目
。
ACTION_OUTSIDE
事件
当使用WindowManager动态的显示一个Modal视图时,可以在显示视图时,指定布局参数的flags为FLAG_WATCH_OUTSIDE_TOUCH
,这样当点击事件发生在这个Modal视图之外时,Modal视图就可以接收到ACTION_OUTSIDE
事件,请参考项目HelloPopupWindow