本篇和触摸事件的分发(View篇)将侧重于Android源码的分析。略显枯燥,但Read the fucking code的code不就是这样吗?
本文代码基于API15分析,而不是最新的API23。因API23中多出的代码和本文无关。
为减少篇幅完整地代码注释会在文末给出链接。
关键的成员变量
触摸目标
TouchTarget
是ViewGroup
的一个静态内部类。描述一个被触摸的 childView 和他所捕获的手指的ids。
private static final class TouchTarget {
public View child;//被触摸到得child
public int pointerIdBits;//由该目标捕获的所有的手指的IDS 的结合位掩码
public TouchTarget next;//用于指向链表中的下一个TouchTarget
}
记录到的全是子View的信息,在处理向子View发送事件的逻辑时使用
触摸目标链表
为了有条理得向子View分发事件,ViewGroup需要记录所有用户触摸到的触摸目标信息。在经过一系列判断逻辑后,向其中的触摸目标分发事件。
这里是通过定义一个TouchTarget
类型的成员变量mFirstTouchTarget
来实现的,其记录了第一个触摸目标,是触摸目标链表的头。通过它(的next
),可以找到链表中所有的触摸目标。
private TouchTarget mFirstTouchTarget;
ViewGroup.dispatchTouchEvent()
ViewGroup中关于事件的分发是通过重写dispatchTouchEvent
实现的。这部分是事件分发过程中最为复杂和难的地方。充分了解了这部分代码。将再也没有难点来阻挡你理解触摸事件的分发了。
ViewGroup中的dispatchTouchEvent()方法非常复杂,不了解整体设计思路,直接阅读将会一头雾水,云里雾里。所以,这里先把ViewGroup的的dispatchTouchEvent()用以下流程图归纳如下:
其中比较关键的有以下几点:
- 触摸事件的安全策略
- 处理最初的DOWN事件
- 检查事件的拦截情况
- 检查是否取消
- 根据需要为DOWN事件更新触摸目标链表
- 分发触摸事件到目标View
- 根据需要,为UP、CANCEL事件更新触摸目标链表
其中,除了第一条相对分发流程比较独立外,其余都标有数字序号,并在图中用蓝色标注出来了。
接下来,我们分别详细讲每一个点。
〇、触摸事件的安全策略
根据用户体验来讲,用户只会尝试去点击可以直接看到的View(或ViewGroup),所以Google据此为触摸事件的分发制定了一个安全策略:
如果某View不处于顶部,并且View设置的属性是该View不在顶部时不响应触摸事件,则不分发该事件。
不满足安全策略需要同时满足两个条件:
- 配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
- 触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
若不满足安全策略,onFilterTouchEventForSecurity(MotionEvent event)
方法返回false,从上文流程图可以看到,这种情况会放弃接下来的所有分发操作。
/**
* 依据安全策略,过滤触摸事件。
* @param event The motion event to be filtered. 需要被过滤的触摸事件。
* @return True if the event should be dispatched, false if the event should be dropped.
* 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
一、处理最初的DOWN事件,重置ViewGroup状态
从Android触摸事件--MotionEvent一文我们可以知道:DOWN类型的事件是一系列触摸事件的开始。
- 从用户角度讲:每次点击屏幕时,必然是首先触发一个DOWN事件。
- 从代码角度讲:ViewGroup最先接收到得必然是DOWN事件,之后才会陆续接收到MOVE、UP或CANCEL等类型的事件。
当上一次触摸结束后,ViewGroup的某些状态可能已经发生了变化,比如ViewGroup的成员变量mFirstTouchTarget
已经记录了一些值。
这时,做了两个动作:
- 取消并清空触摸目标链表。
cancelAndClearTouchTargets(MotionEvent event)
/**
* 取消并清空所有的的触摸目标
* Cancels and clears all touch targets.
*/
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) { //如果mFirstTouchTarget链表不为空,则清空该链表
boolean syntheticEvent = false; //是否是我们人为合成的事件。
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);//人为合成一个 ACTION_CANCEL 事件
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
//从cancelAndClearTouchTargets的调用关系,我们可以发现,这里发送出去的事件只可能是两种:
// 1、处理 DOWN 事件时的DOWN事件
// 2、当该ViewGroup离开屏幕(Window)时,发送上面人为合成的ACTION_CANCEL 消息。
// 但因为第二个参数为true,无论是哪种事件,最终都会被转化为取消事件。
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
if (syntheticEvent) {
event.recycle();
}
}
}
根据Android中ViewGroup源码,该方法除了在这里之外,还会在ViewGroup的dispatchDetachedFromWindow()方法中被调用。这部分逻辑在流程图中特别标示出来了。在分发触摸事件的情况下地逻辑就简单了不少。
1)遍历触摸目标列表,将链表中的所有子View的 CANCEL_NEXT_UP_EVENT 标志全部重置
2)将取消事件传递给链表中所有的子View。(这里事件虽未DOWN事件,但dispatchTransformedTouchEvent方法第二个参数为true,最终都会被转化为ACTION_CANCEL事件)
3)清空触摸链表 clearTouchTargets()
。
- 重置所有触摸状态来为一个新的循环做准备。
resetTouchState();
private void resetTouchState() {
clearTouchTargets();//清空触摸链表
resetCancelNextUpFlag(this);//重置CANCEL_NEXT_UP_EVENT 标志
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置 FLAG_DISALLOW_INTERCEPT 标志
}
这里代码非常简单,就是清空触摸链表、重置本ViewGroup实例的 CANCEL_NEXT_UP_EVENT 标志、重置 FLAG_DISALLOW_INTERCEPT标志。
和cancelAndClearTouchTargets()方法主要处理触摸链表中的子View不同,该方法主要是针对ViewGroup实例自身的一些处理。
二、检查事件的拦截情况
APP开发工程师在开发程序时,可以对ViewGroup是否拦截事件做的限定有:
1、是否允许拦截触摸事件;由 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 来设定
2、是否要拦截某个触摸事件;由onInterceptTouchEvent(MotionEvent ev)返回值来决定
而ViewGroup内部是如何处理这些限定呢?
我们结合代码来看一下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
****代码从略 ****
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//允许拦截
intercepted = onInterceptTouchEvent(ev);//根据onInterceptTouchEvent(dev)决定是否拦截
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//不允许拦截
}
} else {//不是down事件并且mFirstTouchTarget==null也没用与之对应的触摸目标
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 不存在触摸目标,并且该事件不是down事件,那么就继续拦截触摸事件。
//(比如一个空的ViewGroup,则拦截触摸事件,通过自己的touchEvent处理)。
// (又比如虽ViewGroup不为空,但触摸事件并没发生在任何子View上)。
intercepted = true;
}
****代码从略 ****
}
在两种情况下,ViewGroup需要根据APP工程师的限定来决定事件是否被拦截:
- 当发生down事件时
down事件是一个完整事件序列的的起点,当发生down事件时,这时还不知道是否有子View可消费该事件(此时mFirstTouchTarget已经被重置为null),必须根据APP工程师对ViewGroup的限定,方知是否需要拦截该事件。 - mFirstTouchTarget 不为null时
这意味已经(靠该序列的起点down事件)找到要消费触摸事件的目标了,那么肯定不会是down事件了,而是move、up等类型的事件。这时,我们也需要根据APP工程师对ViewGroup的限定来决定是否拦截这种类型(move、up等)的事件。
从代码可知:只有在允许拦截并且onInterceptTouchEvent(MotionEvent ev)返回true时,才会对触摸事件拦截。
在以下情况下,触摸事件默认需要交给ViewGroup自己来处理,这时,我们可以当做事件被拦截了来处理:
- 既不是down事件,此前也没有根据事件序列的down事件找到处理目标。(没有能够找到处理目标的move、up事件,拦截并交给ViewGroup自身来处理)。
比如说一个空的Layout布局;Layout布局不为空,但用户从未点击到任何子View上。
三、检查事件是否被取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
很简单两种情况:
- 当前的View或ViewGroup要被从父View中detach时,PFLAG_CANCEL_NEXT_UP_EVENT就会被设为true;此时,resetCancelNextUpFlag(this)返回true,canceled被赋值为true,它就不再接受触摸事情。
- 触摸事件本身就是MotionEvent.ACTION_CANCEL类型的事件。
四、根据需要,为down事件更新触摸目标链表
- 获取到该触摸事件所对应手指ID,从触摸目标链表中清空与之对应的所有触摸目标。
- 遍历子View,直到找到即可接收触摸事件,触摸事件的坐标又坐落在其坐标范围内的childView;找到了就继续,找不到结束循环。
- 尝试从已有的触摸目标链表中找到与该childView对应的触摸目标实例,找到即结束。
- 没有在已有链表中找到对应的触摸目标实例,就把该事件发送给childView去处理,并生成一个触摸目标实例加入到触摸目标链表中。
- 找不到newTouchTarget,并且触摸目标链表不为空时,将newTouchTarget指向触摸目标链表的最初的target去处理
�1:第一个手指按下(单点触摸)时mFirstTouchTarget链表被清空,肯定找不到
2:多点触摸的后续手指按下时,从前面手指按下时产生的mFirstTouchTarget链表中寻找
五、分发触摸事件到目标。
如果触摸目标链表为空,直接把该ViewGroup当成一个普通的View来处理,
如果触摸目标链表不为空,则遍历链表,将事件分发给触摸目标对应的childView中去。若某childView事件接收到了CANCEL事件,就从链表中移出该触摸目标。
六、处理CANCEL事件和手指抬起事件
-
该ViewGroup接收到了ACTION_CANCEL 事件,或者是最后一个手指抬起时
重置所有触摸状态来为一个新的循环做准备:resetTouchState();
-
多点触摸时,抬起了一根手指(非最后一个)
从触摸链表中移出所有的与该手指有关的TouchTarget,不再考虑任何与该手指相关的操作(因为已经抬起)。final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove);
http://blog.csdn.net/ns_code/article/details/49848801
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/
http://blog.csdn.net/yanbober/article/details/45912661
http://blog.csdn.net/lfdfhl/article/details/42241253
http://www.cnblogs.com/hi0xcc/p/5583791.html
触摸事件的安全策略
/**
* Filter the touch event to apply security policies.
* 依据安全策略,过滤触摸事件。
* 安全策略:
* ①:配置设定被遮挡时需要过滤触摸事件(mViewFlags包含FILTER_TOUCHES_WHEN_OBSCURED)
* ②:触摸事件确实被遮挡(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
* @param event The motion event to be filtered. 需要被过滤的触摸事件。
* @return True if the event should be dispatched, false if the event should be dropped.
* 该事件需要被分发,则返回true。该事件需要被丢弃,则返回false。
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
触发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_DOWN
事件被成功拦截时,那么对于剩下的一系列事件也会被拦截,并且不会再次执行onInterceptTouchEvent方法 - 触摸目标链表只有在
ACTION_DOWN
或ACTION_POINTER_DOWN
事件时才可能被新增,所以,如果一系列事件的后续事件(ACTION_MOVE、ACTION_UP等)要想会被处理,这一系列事件的
疑问
- 在处理最初子View事件时,是否会对触摸链表中的子View都传递DOWN事件。
其他
本篇在将来的某天会有更新。