一、关键类与关键方法
1、MotionEvent
在手指接触到屏幕后会产生一系列的点击事件,如
- 点击屏幕后离开松开,事件序列为DOWN->UP
- 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->…->MOVE->UP 通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到,它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
2、dispatchTouchEvent(MotionEvent ev)
View处理事件的入口,只要事件能到达该View,该方法就会被调用。能到达该View指的是事件在View范围内,且事件未被父View消费。
将事件先后分发给自己与子View处理,如果其中之一消费了该事件,则返回true,告诉上级View该事件已被消费。
3、onInterceptTouchEvent(MotionEvent ev)
返回值表示当前ViewGroup是否拦截该事件,如果拦截了该事件,则不会再将事件分发给子View。并且只要该ViewGroup拦截了事件序列中的一次事件,则后续的事件都不会触发onInterceptTouchEvent(MotionEvent ev)。
4、onTouchEvent(MotionEvent event)
真正开始消费View事件的地方,具体的消费逻辑在其中实现。返回值表示是否消费了该事件。如果返回值表示没有消费该事件,则后续事件也不会到达该方法。
因为父View会维护一个可以消费事件的View的队列,后续事件会直接按照队列顺序发放,而不是以遍历View的方式。
5、ViewGroup/View
在ViewGroup中,上面的三个方法关系可以表述为
1 | public boolean dispatchTouchEvent (MotionEvent ev){ |
在View中,上面的方法关系可以表述为
1 | public boolean dispatchTouchEvent (MotionEvent ev){ |
二、源码分析
1、ViewGroup的事件分发
以下代码均省略了大部分逻辑,只提取了主要逻辑
1 | // ... dispatchTouchEvent(MotionEvent ev) |
2、View的事件分发
1 | // ... dispatchTouchEvent(MotionEvent ev) |
三、一些结论
摘抄自 《Android开发艺术探索》
- 同一事件序列是指手指接触屏幕到离开屏幕中产生的一系列事件。以 down 开始,中间一些 move,最后以 up 事件结束。
- 正常情况下一个事件序列只能被一个 View 拦截且消费。但是可以通过调用其他View的 onTouchEvent 强行传递给其他 View 处理。
- 某个 View 一旦决定拦截,那么这一事件序列都只能由它来处理(如果能传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。
- 某个 View 一旦开始处理事件,如果不消费 ACTION_DOWN 事件(onTouchEvent 返回 false),那么同一事件序列中其他事件都不会交给他处理,并且Down事件重新交给它的父元素去处理,即父元素的 onTouchEvent 会被调用。
- 如果 View 不消费除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 不会被调用,并且当前 View 可以持续收到后续事件,最终消失的事件会传递给 Activity 处理。
- ViewGroup 默认不拦截任何事件。Android 源码 ViewGroup 的 onInterceptTouchEvent 默认返回 false。
- View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递,调用 onTouchEvent 方法。
- View 的 onTouchEvent 默认都会消费事件(返回true),除非它是不可点击的 (clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认 false,click 看情况。Button 的 clickable 默认为 true,TextView 的 clickable 默认为 false。
- View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕 View 的 enable 为 false,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
- onClick 发生的前提是 View 可点击,并且收到了 down 和 up 的事件。
- 事件传递是由外向内的,先传递给父元素再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
四、事件分发流程图
1、点击 View1 区域但没有任何 View 消费事件

2、点击 View1 区域且事件被 View1 消费

3、点击 View1 区域但事件被 ViewGroupA 拦截

五、控制View事件消费过程
在描述事件流控制之前先做一些表述上的约定。
下文出现的圆形表示各种View事件,圆形的颜色,绿色表示该事件被子View消费,蓝色表示该View被父View消费。
而消费则表示该事件的MotionEvent被传递到了父View或者子View的onTouchEvent()中,并且下面的所有表述都默认onTouchEvent()返回了true。实际上除了down事件对onTouchEvent敏感,其他事件的流向并不会受onTouchEvent的影响。
1、事件的消费从子View转移到父View

代码实现:
父View的onInterceptTouchEvent不拦截事件,直到遇到可以拦截的事件,之后后续的事件均会交由父View处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
intercept = couldIntercept();
break;
case MotionEvent.ACTION_UP:
default:
intercept = false;
break;
}
return intercept;
}父View的onInterceptTouchEvent不拦截down,但是拦截所有的move和up,然后子View通过设置父View的FLAG_DISALLOW_INTERCEPT来使父View的拦截是否生效。子View一开始让父View的intercept都失效,直到遇到有效的条件,则使父View的拦截生效,之后后续的事件均会交由父View处理。
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// 子View的代码
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
// 初始不允许父View拦截
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (couldIntercept()){
// 当满足条件时,则使父控件拦截事件
parent.requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
// 父View代码
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
return false;
default:
return true;
}
}2、事件消费从父View转移到子View

从源码上来看,这种情况是无法发生的,因为dispatchTouchEvent的逻辑决定了一旦View开始被父View消费,就不会再分发到子View中,因此想要实现这种事件流,需要去改动dispatchTouchEvent的逻辑。