什么是事件分发机制?
相关方法
事件分发机制相关的几个方法:
View
- dispatchTouchEvent():处理事件。(注:不分发)
- onTouchEvent():触发 onClick() 等点击事件的回调。
onTouchEvent() 在基类 View 的 dispatchTouchEvent() 中被调用。onTouchEvent()在 clickable 或 longclickable 或 contextclickable 时默认返回true,即消耗事件。
ViewGroup
dispatchTouchEvent():分发事件 。
- 判断当前事件是否需要调用 onInterceptTouchEvent() 对事件进行拦截。
- 若 onInterceptTouchEvent() 返回 true 则拦截,并处理事件。否则,则将事件分发给子View。
- 若没有子View 或事件不被下层 View 消耗(即所有子View的 dispatchTouchEvent() 返回false)则调用基类 View 的dispatchTouchEvent() 对事件进行处理。
onInterceptTouchEvent():决定是否拦截事件(即是否分发给子View),返回true 表示拦截,否则不拦截。
onInterceptTouchEvent()(View没有这个方法)在 ViewGroup 的dispatchTouchEvent() 中被调用。onInterceptTouchEvent()默认返回false,即不拦截。- onTouchEvent():继承自 View,不重写。
概念
概念:事件分发机制就是事件(MotionEvent)如何在view tree 中分发、处理的一种规则。
事件分发
概念:一个事件产生后,会先从Activity分发给Window,Window再分发给它里面的顶级View(是view tree 的根)。顶级View接受到事件后会调用自身的dispatchTouchEvent(),在该方法中会迭代调用子View的 dispatchTouchEvent() 直到事件被拦截或消耗(若事件被拦截或消耗则结束分发/遍历),这个过程实质就是对view tree的深度遍历。事件分发只发生在 Activity 和 ViewGroup 中,View 不分发。 Activity 和 ViewGroup 对事件的分发都是通过调用 dispatchTouchEvent() 。
事件分发何时结束?
- 事件被拦截时。
- 事件被消耗时。
- 遍历完整个 view tree 时。
事件拦截:即 onInterceptTouchEvent() 返回值是 true。只有ViewGroup才能拦截事件。
事件处理结果:即 dispatchTouchEvent() 返回值,true 表示消耗,false表示不消耗。
事件消耗:即 dispatchTouchEvent() 返回值是 true。事件处理
事件可以在 Activity 或 ViewGroup 或 View 中被处理,而且可被多次处理直到被消耗才停止处理。Activity:对事件的处理是通过调用自身的 onTouchEvent()。
View:View 接收到事件后会调用 dispatchTouchEvent() 直接对事件进行处理。在该方法里会触发onTouch() 或 onTouchEvent() 对事件进行处理。
ViewGroup:若没有重写dispatchTouchEvent(),都是通过调用基类 View 的 dispatchTouchEvent() 并将其返回值作为该 ViewGroup 的 dispatchTouchEvent() 的返回值。ViewGroup 何时对事件进行处理?
1、拦截事件或没有子View 时。
2、下层的所有View 都未消耗事件时。View 何时对事件进行处理?
View 接受到事件后,(不会进行分发,因为没有子View)会直接处理事件,并将事件处理结果返回给它的ViewGroup。
注:事件处理不等于事件消耗。
事件分发
事件在view tree中分发的流程(图解)
“分发树”:当触碰事件产生时,ACTION_DOWN 产生时所触摸到的所有 View 按照父子关系可以组成一个 view tree 即“分发树”,“分发树”的根节点是顶级View,“分发树”是整个Window 中完整的 view tree 的一部分,ACTION_DOWN 就是在这个“分发树”中分发的。
事件在view tree中分发的流程:
ACTION_DOWN 事件
ACTION_DOWN 在具体应用程序app区域下分发的几种常见情形:
不了解Android 手机界面组成参考:Android手机界面组成
总之,ACTION_DOWN 会深度遍历“分发树”并确定“消耗树”。注:这里的“消耗树”指的就是上图中的“消耗路径”。后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)
后续同一序列事件都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。
若ACTION_DOWN找不到“消耗路径”(即不被任何View消耗)也不被Activity消耗,那么后续事件就会消失,不会被处理。
“消耗路径”:从顶级View到消耗事件的View的最短路径就是“消耗路径”(线性结构的),该“消耗路径”会与已有的“消耗树”合并(若存在“消耗树”的话。通常情况下是没有的,因为ACTION_DOWN时会清空)。
“消耗树”:一条或多条“消耗路径”合并形成的。“消耗路径”也是“消耗树”的一种,只不过它只有一条“消耗路径”。
同一个事件序列:指从手指接触屏幕(触发ACTION_DOWN事件)到手指离开屏幕(触发ACTION_UP)以及期间产生的一系列事件。
注意:同一事件序列只有一条“消耗路径”,因此,觉得混乱的话可以直接把“消耗树”当做“消耗路径”。
下面是将“分发树”简化为线性结构后事件分发的几种情形:
红色:代表ACTION_DOWN事件的分发路径,不是“消耗路径”。
蓝色:代表ACTION_MOVE 和 ACTION_UP 事件分发路径,也是“消耗路径”。
1、我们重写ViewGroup1 的dispatchTouchEvent 方法,直接返回true消费这次事件
ACTION_DOWN 事件从(Activity的dispatchTouchEvent)——–> (ViewGroup1 的dispatchTouchEvent) 后结束传递,事件被消费(如下图红色的箭头代码ACTION_DOWN 事件的流向)。
2、我们在View 的onTouchEvent 返回true消费这次事件
3、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件
4、我们在Activity 的onTouchEvent 返回true消费这次事件
5、我们在View的dispatchTouchEvent 返回false(即重写并直接返回false)并且Activity 的onTouchEvent 返回true消费这次事件
上面例子来源:图解 Android 事件分发机制
注:下面的源码都来自Android-23版本。
事件在各结点中分发的流程(图+源码)
Activity
流程图
Activity的 dispatchTouchEvent() 的流程:
源码
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
Activity对事件的分发是通过Window来分发的,并不拦截。
注意:Window只负责帮Activity分发事件,不处理,也就是没有onTouchEvent方法,可以把它的分发功能当成Activity的一部分,然后忽略它。
ViewGroup
View和ViewGroup中的方法:
View:dispatchTouchEvent();onTouchEvent()
ViewGroup:重写dispatchTouchEvent();onInterceptTouchEvent();
注意:ViewGroup及其子类并未重写onTouchEvent()。流程图
ViewGroup的dispatchTouchEvent方法的流程:
TouchTarget链表:每个ViewGroup都持有一个mFirstTouchTarget变量,该变量指向一个TouchTarget链表(不带头结点)的首结点,该链表的结点用于存放当前Viewgroup的子结点,该子结点必须是“消耗树”上的结点。TouchTarget链表会在ACTION_DOWN被清空即“消耗树”消亡。总之,ViewGroup 的 TouchTarget链表存放该 ViewGroup 在“消耗树”上的所有子结点,mFirstTouchTarget指向它的首结点。
事件在 ViewGroup 中分发的流程(即 ViewGroup 的 dispatchTouchEvent() 的流程):
若为 ACTION_DOWN 事件:
是否拦截
当前 ViewGroup 结点若拦截事件则结束分发,否则会将事件分发给它的子View。只分发给“分发树”上的子结点
遍历 Window 中完整的 view tree,若当前结点不在触碰区域内则进入下一结点,不对其分发事件。其实就是只在“分发树”上分发。
注:在事件是 pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等)时会形成一个“分发树”。更新 TouchTarget 列表
在遍历它在“分发树”上的子结点时,若遍历到的子结点在当前 ViewGroup 的 TouchTarget 链表中则将事件分发给该子结点(即调用该子结点的dispatchTouchEvent())并结束对当前 ViewGroup 的子结点的遍历,否则,遍历每个子结点,调用每个子结点的 dispatchTouchEvent()直到被消耗即返回 true 才结束遍历并将该子结点插入到 TouchTarget 链表的头部(其实就是合并到已有的“消耗树”上,如果有“消耗树”的话)。总之,遍历子结点时,若子结点在已有的“消耗树”上则分发给它并结束更新,否则调用子结点的 dispatchTouchEvent(),若返回true则将该子结点插入链表头部结束更新。分发。
遍历 TouchTarget列表,调用每个元素(即该 ViewGroup 在“分发树”上的每个子View)的 dispatchTouchEvent() 。
若是后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP):
是否拦截
“消耗树”上的 ViewGroup 结点若拦截事件则结束分发,否则会将事件分发给它的子View。遍历TouchTarget列表,调用每个元素(即该 ViewGroup 在“消耗树”上的每个子View)的 dispatchTouchEvent() 。
是否拦截:当 ViewGroup 的 disallowIntercept 为 false 也就是允许拦截时,若事件为ACTION_DOWN 或mFirstTouchTarget != null 必定触发当前ViewGroup的onInterceptTouchEvent()。滑动冲突的处理就是在 ACTION_MOVE 事件在“消耗树”上分发时在中途将它拦截,参考:Android 滑动冲突的处理。
源码
public boolean dispatchTouchEvent(MotionEvent ev) {
//关键步骤:
//1、是否拦截事件。
//2、更新TouchTarget 列表。
//3、分发。
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
......
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
//每次事件序列开始(即发生ACTION_DOWN事件时)都会
//重置mFirstTouchTarget、mGroupFlags的值为初始值(分别为null 和 允许拦截)。
resetTouchState();
}
//1、是否拦截事件
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//mFirstTouchTarget指向一个TouchTarget列表的首结点。
//TouchTarget列表是Linked List,若事件分发给某个子View后可被“下层”消耗,
//则添加该子View到列表头部(采用的是头插法)。
//“下层”包括当前ViewGroup里面的所有在当前事件触摸范围内的View,不仅仅指它的子View。
//简单的说就是当前事件之前的事件被下层消耗则不为null。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//disallowIntercept是否不被允许拦截事件。
//可通过子元素调用requestDisallowInterceptTouchEvent(boolean b)来
//设置mGroupFlags的值,从而改变disallowIntercept。
//但是这一条件判断对ACTION_DOWN无效(总为true),因为会重置mGroupFlags(见上面代码)。
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//下面的代码都是讲事件是如何往下层分发的,分两步:2、3
//2、更新TouchTarget 列表,事件为pointer down时才需要。
//pointer down包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等,参考下面判断条件。
//注意:添加的元素必须是事件范围内也就是触摸到的区域内的子View,同时,沿着这个子View往下分发能消耗事件,
//或者说,调用该子View的dispatchTouchEvent方法返回结果是true。
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//非canceld && 不拦截 时才向下层分发事件
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//注意:以下几种情况才需要更新TouchTarget 列表
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//遍历子元素
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
//若该View不被触碰到或不能相应事件则剔除,其实就是只允许对“分发树”遍历
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {//子元素已经在TouchTarget链表中,结束遍历。
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//dispatchTransformedTouchEvent()会调用子元素的dispatchTouchEvent()。
// Child wants to receive touch within its bounds.
......
//采用头插法将子元素添加到TouchTarget列表中
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//已找到消耗此事件的路径且已分发给下层的目标View
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
//并未找到消耗事件的路径 && mFirstTouchTarget 不为空时,将TouchTarget链表中
//最近最少被添加的target赋给它,即将链表的最后一个结点的引用赋给newTouchTarget。
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//3、分发:往下层分发事件。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//当做普通View对待,而不是ViewGroup。
//会调用super.dispatchTouchEvent()方法,最终调用自身的onTouchEvent方法.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//在上面更新TouchTaget列表时已分发完毕。
handled = true;
} else {//在前面同一事件序列的事件的消耗路径上往下分发。
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
return handled;
}
关键步骤:
1、是否拦截事件。
- 当ViewGroup的disallowIntercept为false也就是允许拦截时,若事件为ACTION_DOWN 或mFirstTouchTarget != null 一定会触发当前ViewGroup的onInterceptTouchEvent()。
- 子元素能够通过调用requestDisallowIntercept(boolean b)来控制父容器能否调用onInterceptTouchEvent()。
2、更新TouchTarget 列表。
- 本质是寻找该事件的“消耗路径”的下一个结点。若TouchTarget列表中有元素处于当前事件的触摸区域内,则结束更新,否则会遍历调用触摸区域内的子View的dispatchTouchEvent方法,直到返回true并添加到列表。。
- 事件属于pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等),则要更新列表。
- 更新列表规则:添加的元素必须是事件范围内也就是触摸到的区域内的子View,同时,沿着这个子View往下分发能消耗事件,或者说,调用该子View的dispatchTouchEvent方法返回结果是true。
- 更新列表的步骤:遍历判断 1、子View是在触摸区域内,不是则continue,进入下一个循环。2、子View是否已在列表中,是则break,结束遍历。3、调用子View的dispatchTouchEvent方法,返回结果是true则用头插法加入列表(在这一步中已进行分发,后面的“分发”代码不会再分发一次)。
3、 分发。
- 遍历TouchTarget列表,调用每个元素的dispatchTouchEvent方法。
总结:ACTION_DOWN若能被消耗则会确定“消耗树”,后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿着这一“消耗树”分发的,且可被中途拦截。
ViewGroup的分发过程可用如下伪代码表示:
dispatchTouchEvent(MotionEvent ev){
boolean consume = false;//事件是否被消耗,若被消耗则该事件的分发结束
if (onInterceptTouchEvent(ev)) {//拦截事件
consume = super.dispatchTouchEvent(ev);//即View.dispatchTouchEvent(ev)
}else {
//遍历子元素,将事件分发给子元素,直到事件被消耗。
//其实,实际代码只需遍历TouchTarget列表中的元素,不需要遍历所有子View。
View child = null;
for (int index = 0; index < childNum; index++) {
child = getChild(index);
if (null != child) consume = child.dispatchTouchEvent(ev);
if (consume) break;
}
//遍历结束但事件没有被消耗,对事件进行处理。
if (!consume) consume = super.dispatchTouchEvent(ev);
}
return consume;
}
View
View 的 dispatchTouchEvent() 直接处理事件,不分发。
事件处理
Activity
Activity 对事件的处理都是通过调用自身的 onTouchEvent() 。
源码:
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
View
无论View 还是ViewGroup,若没有重写dispatchTouchEvent(),对事件的处理都是通过调用基类View的 dispatchTouchEvent(),最终调用 onTouch() 或 onTouchEvent() 。
基类 View 的 dispatchTouchEvent() 流程:
若View设置了 OnTouchListener 且 onTouch() 返回 true 则dispatchTouchEvent() 返回 true,不会调用onTouchEvent()。否则,会调用 onTouchEvent() 且 onTouchEvent() 的返回值就是dispatchTouchEvent() 的返回值。
在View的onTouchEvent()中若View是clickable或longclickable的则会调用onClick()(若有设置OnClickListener)。clickable或longclickable或contextclickable时默认返回true,否则返回false。
流程图
View的dispatchTouchEvent方法的流程:
源码
public boolean dispatchTouchEvent(MotionEvent event) {
......
boolean result = false;//是否消耗事件
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//若设置了OnTouchListener,则先调用onTouch()。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//若onTouch()没有消耗事件则调用onTouchEvent()
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
若View是enabled的,且设置了OnTouchListener,则先调用onTouch(),若onTouch()返回true则分发结束,否则,接着调用onTouchEvent()。
ViewGroup
ViewGroup对事件处理是通过调用基类View的dispatchTouchEvent() ,最终调用 onTouch() 或 onTouchEvent() 。
ViewGroup的onTouchEvent()继承自View,本身不重写。ViewGroup的实现类也不重写该方法。
总结
事件分发机制概要流程:
上面图片来源:Android事件分发机制 详解攻略,您值得拥有
事件分发机制详细流程:
总之,ACTION_DOWN 会深度遍历“分发树”并确定“消耗树”,后续同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。