Android事件分发知识点整理

了解MotionEvent

MotionEvent.class自身并不实际包含Action相关的信息,只包含了一个mNativePtr,指向JNI层实际包含所有Event信息的Object。

Pointer

  • Pointer指多点触控中的一个点,通常就是一根手指。
  • 一般第一个按下的Pointer即为Primary Pointer,其他的为Non-Primary Pointer。
  • PointerIndex,即Pointer的Index,用于区分不同的Pointer。

Action

View在响应一次用户操作时,会接收到一个事件流,以ACTION_DOWN开始,中间有若干个ACTION_MOVE、ACTION_POINTER_DOWN、ACTION_POINTER_UP,最后以ACTION_UP或者ACTION_CANCEL结束。

4个基本事件如下:

  • ACTION_DOWN:第一个Pointer按下。获取到的是最初始状态下的信息。此时View一般会设置一些标志位,表明自己被按下了,开始处理用户事件。如果有设置selector,此时selector也会变成pressed状态。
  • ACTION_MOVE:有一个Pointer移动。由于事件分发有时间间隔(包括可能发生卡顿),实际获取到的数据是离当前最近的时间点的数据。通常在ACTION_MOVE事件发生时处理滑动操作。
  • ACTION_UP:最后一个Pointer释放。通常在此时处理点击操作,或者作为滑动操作的终止条件,并开始执行惯性滑动(Fling)过程。
  • ACTION_CANCEL:操作被取消,通常是因为事件被其他组件处理了。收到ACTION_CANCEL的组件应该及时清除按下状态,且不作出事件响应。具体场景可以参考后文的实际案例。

多点控触新增的Action:

  • ACTION_POINTER_DOWN:一个Non-Primary Pointer按下
  • ACTION_POINTER_UP:一个Non-Primary Pointer释放

事件API以及单点、多点触控的兼容问题

1、早期Android只支持单点触控,View中通过getAction()获取int型的事件(Action),然后用switch-case判断即可。

1
2
3
4
5
6
// MotionEvent event
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
}

2、高版本Android系统中增加了多点触控机制,如果直接用getAction()获取int型action值,其中最低8位即0-7位用于存储原有的Action,8-15位用于在ACTION_POINTER_DOWN/UP事件发生时存储PointerIndex。系统提供了新的API,一般应该使用getActionMasked()获取低8位的Action部分,通过getActionIndex()获取8-15位的PointerIndex

3、多点触控机制可以兼容单点触控的View组件。也就是说,在高版本Android系统提供了多点触控机制之后,原有的单点触控组件仍然能正常运行。原因是原有的4个基本事件发生时,8-15位全为0,并没有PointerIndex,用getAction()getActionMasked()得到的值相同。

坐标、压力、大小

  • getX(),getY()可获取MotionEvent相对当前View的坐标
  • getRawX(),getRawY()可获取MotionEvent相对屏幕的坐标
  • getPressure(),getSize()等可获取压力、大小等信息,需要硬件支持

Pointer相关API

  • 通过MotionEvent.getPointerCount(),可以获取当前有几个Pointer处于按下状态。
  • 通过getPointerId(int pointerIndex)findPointerIndex(int pointerId)可以互相转换PointerIndex和PointerId。
  • getX(int pointerIndex),getY(int pointerIndex)等方法可以获取指定PointerIndex的坐标、压力、大小等信息。

生成精确的MotionEvent用于调试

开发过程中,常需要调试交互组件,但手工操作比较麻烦,耗时耗力,精度低,可能满足不了需要。这时就可以考虑使用代码自动产生精确的MotionEvent序列用于调试。

例如可以用下面的方法,产生精确的滑动事件序列,直接调用Activity.dispatchTouchEvent注入事件,观察UI界面的响应。

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
private static Handler handler = new Handler(Looper.getMainLooper());

// 向Activity分发移动事件 ACTION_DOWN --> MOVE --> MOVE --> ... --> MOVE --> UP
public static void moveAction(final Activity activity, int startX, int startY, int stepX, int stepY, int stepT, int eventCount) {
if (eventCount < 3) return;
final long downTime = System.currentTimeMillis();

int offsetTime = 0;
for (int i = 0; i < eventCount; i++) {
final int action;
if (i == 0) {
action = MotionEvent.ACTION_DOWN;
} else if (i == eventCount - 1) {
action = MotionEvent.ACTION_UP;
} else {
action = MotionEvent.ACTION_MOVE;
}
final long eventTime = downTime + offsetTime;
final int x = startX;
final int y = startY;
final int index = i;
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i("Event", String.format("----> send event(%d): xy = (%d, %d)", index, x, y));
activity.dispatchTouchEvent(MotionEvent.obtain(downTime, eventTime, action, x, y, 0));
}
}, offsetTime);
startX += stepX;
startY += stepY;
offsetTime += stepT;
}
}

事件分发流程

事件分发到View前经过的几个关键方法:

1
2
3
4
ViewRootImpl.ViewPostImeInputStage.processPointerEvent
PhoneWindow.DecoreView.dispatchTouchEvent
Activity.dispatchTouchEvent
ViewGroup.dispatchTouchEvent

View中的关键函数调用关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
ViewGroup.dispatchTouchEvent {
ViewGroup.onInterceptTouchEvent
ViewGroup.dispatchTransformedTouchEvent {
child.dispatchTouchEvent
super/View.dispatchTouchEvent {
View.OnTouchListener.onTouch
View.onTouchEvent {
View.TouchDelegate.onTouchEvent
View.performClick
}
}
}
}

requestDisallowInterceptTouchEvent

当子View不希望父View拦截事件时,可以调用父View的requestDisallowInterceptTouchEvent方法。

DescendantFocusability

DescendantFocusability用于设置嵌套View的焦点获取优先顺序,通常只有获取的焦点的View才能响应点击事件。

  • blocksDescendants
  • beforeDescendants
  • afterDescendants

AbsListView嵌套AbsListView、CheckBox、Button、ImageButton、设置了OnClickListener的View,焦点默认分发给子View,导致OnItemClickListener无效。此时可以将AbsListView设置成blocksDescendants。

TouchDelegate

TouchDelegate可扩大一个View的触摸响应范围,使其可以响应超过自身大小范围的事件。

子View在onLayout获取尺寸后,给Parent设置一个TouchDelegate,即可扩展自身点击区域。这个区域不能超过Parent的点击区域;且每个Parent只能设置一个TouchDelegate,指定给一个子View扩展点击区域。

更复杂的需要,可以通过覆写TouchDelegate,或者覆写Parent的事件处理方法实现。

VelocityTracker

VelocityTracker用于计算滑动速度。几个关键方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 静态方法,创建实例
VelocityTracker.obtain();
// 回收对象
VelocityTracker.recycle();

// 清除事件
VelocityTracker.clear();
// 添加一个MotionEvent
VelocityTracker.addMovement(MotionEvent ev);
// 计算速度,units通常取1000,表示计算每秒移动多少个像素点
VelocityTracker.computeCurrentVelocity(int units, float maxVelocity);
// 获取X、Y方向滑动速度
VelocityTracker.getXVelocity();
VelocityTracker.getYVelocity();

ViewConfiguration

ViewConfiguration接口中定义了一系列和View操作相关的阈值。

TouchSlop

当用户点击屏幕上的一个View时,除了ACTION_DOWNACTION_UP,一般情况下,由于手指操作并不十分精确,中间可能会产生若干个ACTION_MOVE,且ACTION_DOWNACTION_UP事件的坐标通常并不完全相同。

如何区分用户是点击还是滑动操作呢?Android中使用TouchSlop来解决这个问题。当移动的距离小于等于TouchSlop时,视为点击操作;大于TouchSlop,则视为滑动操作。这里的距离,可能是X方向、Y方向,视具体的View组件而定。

在Android源码中,一般TouchSlop被定义为8dp对应的像素点,实际根据不同手机ROM而定。

PagingTouchSlop

和TouchSlop类似的还有一个PagingTouchSlop,这个通常用于ViewPager中判断用户滑动距离是否可以视为翻页操作。

其他

ViewConfiguration还定义了其他一些值,例如按下多长时间可以认为是长按操作,速度达到多大可以视为快速滑动(Fling)等。

GestureDetector

GestureDetector提供了一套比较简单的接口,实现常用手势检测。

案例一:自己实现一个OnClickListener

覆写View的onTouchEvent,或设置OnTouchListener。

方案一:

  1. ACTION_DOWN时记录下初始坐标mStartX,mStartY
  2. ACTION_UP时获取事件坐标x,y,分别判断x、y两个方向相对初始坐标mStartX,mStartY的位移均没有超过TouchSlop,则可视为点击事件,此时即可触发OnClickListener。如果超过了TouchSlop则视为滑动事件,不会触发点击事件。

方案二:

方案一有个小问题,如果用户手指在View组件上滑动了一段距离然后又滑动回来然后释放,且ACTION_DOWN和ACTION_UP的坐标恰好接近,此时还是会被视为点击操作。改进的方案二如下:

  1. ACTION_DOWN时记录下初始坐标mStartX,mStartY,并设置标志位mIsBeingDragged为false。
  2. 每个ACTION_MOVEACTION_UP事件生成时判断位移相对初始坐标是否超出TouchSlop,如果超过了则设置标志位mIsBeingDragged为true。
  3. ACTION_UP时判断标志位mIsBeingDragged如果为false,则视为点击事件。

案例二:ScrollView嵌套Button的事件分发

ScrollView嵌套一个Button。

场景1

  • 当手指按下按钮时,按钮处于按下状态;
  • 手指释放,Button被点击。

场景2

  • 当手指按下按钮时,按钮处于按下状态;
  • 手指开始进行滑动,可以观察到,按钮的按下状态消失,ScrollView开始滚动。

这个两个场景中的事件分发过程如下:

  1. ACTION_DOWN被传递给ScrollView.dispatchTouchEvent,然后调用ScrollView.onInterceptTouchEvent,返回值为false。于是ScrollView会把ACTION_DOWN分发给Button。
  2. Button接收到事件后,设置自己处于按下状态,于是selector会展示成selected状态。
  3. 手指开始滚动,连续产生多个ACTION_MOVE,其垂直方向相对ACTION_DOWN的位移没有达到TouchSlop,事件经过ScrollView.dispatchTouchEventScrollView.onInterceptTouchEvent,然后被分发给Button。
  4. 对于场景1,在ACTION_MOVE后,产生了一个ACTION_UP事件,并被分发给Button,于是Button.performClick被执行,从而OnClickListener被调用。流程结束。
  5. 对于场景2,手指继续滑动直到某一次ACTION_MOVE位移达到TouchSlop,ScrollView.onInterceptTouchEvent会设置mIsBeingDragged=true并返回true,此时ScrollView.dispatchTouchEvent判断发现之前已经把事件分发给Button了,于是给Button分发一个ACTION_CANCEL事件,同时把这个ACTION_MOVE分发给自己的ScrollView.onTouchEvent
  6. Button接收到ACTION_CANCEL事件后,取消按下状态,且不触发OnClickListener
  7. ScrollView.onTouchEvent接收到ACTION_MOVE事件后,开始滚动。
  8. 由于mIsBeingDragged==true,之后的ACTION_MOVEACTION_UP都会被发送给ScrollView.onTouchEvent
  9. ACTION_UP事件发生时,ScrollView会获取VelocityTracker记录的滚动速度,然后利用Scroller执行Fling过程,即惯性滚动。流程结束。

场景2输出的Log如下:

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
event______: ACTION_DOWN, xy = (370.5, 1147.1)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onInterceptTouchEvent
m_LogScroll: onInterceptTouchEvent return false
m_LogButton: dispatchTouchEvent
m_LogButton: onTouchEvent
m_LogButton: onTouchEvent return true
m_LogButton: dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (370.5, 1140.4)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onInterceptTouchEvent
m_LogScroll: onInterceptTouchEvent return false
m_LogButton: dispatchTouchEvent
m_LogButton: onTouchEvent
m_LogButton: onTouchEvent return true
m_LogButton: dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (370.5, 1124.0)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onInterceptTouchEvent
m_LogScroll: onInterceptTouchEvent return true
event______: ACTION_CANCEL, xy = (370.5, 1124.0)
m_LogButton: dispatchTouchEvent
m_LogButton: onTouchEvent
m_LogButton: onTouchEvent return true
m_LogButton: dispatchTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (372.5, 1088.7)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (375.5, 1056.0)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (378.2, 1038.7)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1025.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_MOVE, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll:
event______: ACTION_UP, xy = (377.5, 1017.2)
m_LogScroll: dispatchTouchEvent
m_LogScroll: onTouchEvent
m_LogScroll: onTouchEvent return true
m_LogScroll: dispatchTouchEvent return true
m_LogScroll: