前言
关于事件分发机制,这个东西对于开发者,很重要,例如:解决滑动冲突;对于面试者,也很重要,比如:请描述一下View的事件分发机制~。说句实话,这个源码是看了很长时间,一直不敢写这篇文章,生怕误人子弟啊,说实话,刚开始刚觉很难,但是硬着头皮再看,也就那么回事了。。。
View分发机制的相关方法
View
- dispatchTouchEvent(ev) : 专门处理事件
- onTouchEvent(ev) :
ViewGroup
- dispatchTouchEvent(ev) : 它做的也就两件事,首先判断是否拦截事件;如果不拦截的话,就是往下分发事件给子View。
- onInterceptTouchEvent(ev) : 是否拦截该事件,如果true,交给自己的onTouchEvent处理;false,就表示不拦截该事件
- onTouchEvent(ev) : ViewGroup内部没有重写该方法;
注意:这里的事件指的是:从手指触碰ViewGroup到离开ViewGroup的一系列事件。
View分发流程
假如此时有两个ViewGroup_1, ViewGroup_2以及一个View,其中 ViewGroup_1包含ViewGroup_2, ViewGroup_2包含View,那么此时默认情况下事件分发的流程就是:
ViewGroup_1——dispatchTouchEvent
ViewGroup_1——onInterceptTouchEvent
ViewGroup_2——dispatchTouchEvent
ViewGroup_2——onInterceptTouchEvent
View——dispatchTouchEvent
View——onTouchEvent
ViewGroup_2——onTouchEvent
ViewGroup_1——onTouchEvent
上个图:
解释一波上图流程:
1:默认情况下,先调用ViewGroup的dispatchTouchEvent()方法 ,如果返回true,就不再往下分发,如果是默认值(false),那么调用onInterceptTouchEvent();
2:如果onInterceptTouchEvent()方法返回true,代表拦截事件,那么就交给自己的onTouchEvent()处理事件;如果返回默认值,那么就表示不拦截事件,继续向下分发,将事件交给子ViewGroup或者子View;
3:如果child是ViewGroup,那么递归1,2步骤。 如果child是View, 那么就先调用View 的dispatchTouchEvent事件,如果不消耗事件,那么调用其onTouchEvent事件,如果不消耗事件,那么调用父ViewGroup的onTouchEvent事件。换句话说:隧道式分发,冒泡式消费!
其实看到这里,建议各位还是先明白每个方法返回什么值,会产生什么样的效果,这样接下来看源码会舒服很多。
ViewGroup源码分析
我们点击的Activity,首先进入到Activity的dispatchTouchEvent源码:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到首先接收到ACTION_DOWN的时候,首先调用的是自己的onUserInteraction方法,onUserInteraction方法是个空的实现,用户可以重写该方法,处理事件开始分发的相关逻辑,总体来说就是告诉开发者:我要开始事件分发了啊!
接下来调用了window的superDispatchTouchEvent方法,如果该方法返回false(不消耗事件),那么就调用自己的onTouchEvent方法;好,接下来看下getWindow().superDispatchTouchEvent(ev),那么window是什么时候初始化的时候呢?
是在Activity中的attach方法中初始化的:
final void attach(Context context, ActivityThread aThread,
.......
//创建PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
........
}
好,到这里我们知道了getWindow().superDispatchTouchEvent(ev)实际上调用的是PhoneWindow的方法,另外可以看下Window的注释,注释说的很清楚:PhoneWindow是Window的唯一实现。
进入到PhoneWindow的superDispatchTouchEvent方法:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
这里的mDecor也就是DecorView对象,而DecorView也是继承自FramLayout,里面装的是我们显示的布局,也就是setContentView(View),在进入到DecorView的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
调用了父类的dispatchTouchEvent方法,所以从触摸Activity开始路径为:activity——phonWindow——DecorView——ViewGroup——我们自己的布局。
终究调用的是ViewGroup的dispatchTouchEvent(ev)方法,进入到源码:
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
上述代码首先就是清空标记,也就是当每一次是down手势的时候,就清空标记,清空什么标记呢?
1:mFirstTouchTarget值null
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
2:重置FLAG_DISALLOW_INTERCEPT标记 (mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
)。
至于这两个标记是什么下文看。
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
那么根据我们对事件分发结果观察,ViewGroup执行dispatchTouchEvent(ev)之后就是执行了onInterceptTouchEvent(ev)方法,当然了,条件是默认情况下。
根据上述结果,看源码:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否不允许拦截
//disalllowIntercept的值可以通过requestDisallowInterceptTouchEvent设置,默认值是false,
//如果是true,!disallowIntercept = false, 此时:走else intercepted = false;,也就是不拦截事件,不执行onInterceptTouchEvent方法
//默认是false,此时!disallowIntercept = true,也就是说:子View可以让父View拦截事件。此时走if, 执行onInterceptTouchEvent方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//调用ViewGroup的onInterceptTouchEvent方法
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;
}
清空标记之后,如果是Down手势或者mFirstTouchTarget != null 就有机会执行onInterceptTouchEvent方法,那么想一下,首次执行Down手势,肯定能进入if执行里面的代码。 disallowIntercept标记就是子View调用requestDisallowInterceptTouchEvent(boolean)设置的,我们一般处理滑动冲突是不是经常用到这个方法?!,怎么生效的呢? 慢慢看。刚开始disallowIntercept不允许父View拦截事件,那么!disallowIntercept就执行onInterceptTouchEvent(ev)方法,这就印证了刚开始我们说的View’Group执行dispatchTouchEvent之后就是执行onInterceptTouchEvent方法。
现在拦截事件也触发了,是不是该执行子ViewGroup的dispatchTouchEvent事件了?
源码接着看:
//接下来的主要代码就是想子View'Group(或者子View)分发事件
//TouchTarget 是一个链表,这个链表内存储的是子View,根据存储的子View,一步一步的
//找到消耗事件的子View,总体来说,TouchTarget 可以理解为消耗路径。
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//不取消 && 不拦截
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//手势是:ACTION_DOWN || ACTION_POINTER_DOWN || ACTION_HOVER_MOVE成立
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
.....
//newTouchTarget就是链表的头节点,跟mFirstTouchTarget会互相赋值
//如果子View的数量不为null
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//循环遍历子View(ViewGroup)
for (int i = childrenCount - 1; i >= 0; i--) {
.......
//触摸的点不在子View内 || 触摸的子View有在执行动画
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
//结束本次循环
continue;
}
// child是否在链表中(消耗树)中,如果在,就结束for循环;
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 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;
}
resetCancelNextUpFlag(child);
// 此处是个递归调用,dispatchTransformedTouchEvent内部就是分发事件的实现,下文看源码,
//以下的代码核心就是:寻找消耗路径。当dispatchTransformedTouchEvent返回true的时候,也就是说有子View消耗的时候,会递归返回true,此时会将子View(ViewGroup)依次添加到链表TouchTarget中,然后返回链表的头节点:newTouchTarget。
//沿着这个消耗路径,也就是遍历TouchTarget的话,可以找到消耗事件的子节点。
//那么,如果dispatchTransformedTouchEvent返回false呢,也就是说没有子节点消耗事件呢?下文细说
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//**强调:在addTouchTarget方法内,将mFirstTouchTarget赋值,可自行查看!!!**
newTouchTarget = addTouchTarget(child, idBitsToAssign);
//已经找到消耗路径了(事件已经被自己的子节点消耗了)
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
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;
}
}
}
上述代码总体流程是这样的:
先判断是否是down手势,如果是,就for循环ViewGroup的子节点,判断触摸的点是否在子View范围内以及子View是否正在执行动画,如果在子View范围内 && 子View没有在执行动画,在看下第一个child是否在消耗路径中,如果在,就直接跳出for循环,如果不在,那么就开始一层层分发事件,知道找到消耗事件的子View(viewGroup)返回true,然后再递归更新链表(消耗路径)。那么至于事件到底是怎么分发给子节点的呢?看下dispatchTransformedTouchEvent源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
......
// Perform any necessary transformations and dispatch.
if (child == null) {
//如果child是null,就调用自己父View的dispatchTouchEvent方法,说白了就是吧ViewGroup当成了View处理
//因为ViewGroup继承自View,所以调用的还是View的dispatchTouchEvent方法
handled = super.dispatchTouchEvent(transformedEvent);
} else {
.......
//如果child不是null,就调用子节点的dispatchTouchEvent(ev); 递归调用
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
从上述代码可以看出,当for循环分发事件的时候,child是不为null的,此时调用的子节点的dispatchTouchEvent方法,深度遍历。
至此深度遍历寻找消耗路径已经完成(假设子View消耗了事件),好,想一下,现在是down手势已经分发完成了,那么move手势和up手势呢?怎么分发呢?
回头看一下上述源码,最外层先判断的就是是否是down事件,如果是down手势,上述代码的事件分发都是基于这三个手势的: ACTION_DOWN || ACTION_POINTER_DOWN || ACTION_HOVER_MOVE ,才能进入if分发事件,此时move手势和up手势怎么分发呢? 想一下,我们不是有targetTouch链表吗? 根据这个路径可以找到消耗事件的子节点。接下来的源码应该就是水到渠成了:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 没有子节点处理事件,把ViewGroup自己当成普通的View处理
//注意此时传递的child值为null,
// No touch targets so treat this as an ordinary view.
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) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//往下分发事件(move, up)
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;
上述源码也就是遍历链表,然后每个子节点分发事件,深度遍历,完成除了down手势之外的一系列手势。
至此我们的分析完成一部份了,回顾一下我们分析的场景:该View’Group的子节点(或者孙节点)消耗了Down手势,那么更新链表,再通过循环链表完成其它手势的分发。
好,此时考虑一个问题,如果该ViewGroup的子节点(或者孙节点)没有消耗事件, 那么也就是说Down手势判断代码里面的dispatchTransformedTouchEvent为false, 在换句话说,也就是链表为null,没有找到消耗路径,那么该怎么办呢?
还记得我们刚进入ViewGroup的时候将mFirstTouchTarget置为了null, 然后再addTouchTarget方法内,将mFirstTouchTarget赋值,那么此时根本都没有子节点消耗事件,所以addTouchTarget方法肯定进不去,那么自然而然的mFirstTouchTarget也就是null了,那么再看回头看下move和up手势的分发源码:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 没有子节点处理事件,把ViewGroup自己当成普通的View处理
//注意此时传递的child值为null,
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...........
}
当mFirstTouchTarget =null的时候,调用的是dispatchTransformedTouchEvent,其中child的参数的值是null,
再回顾一下dispatchTransformedTouchEvent源码,此时child = null, 那么就会把ViewGroup当成了普通的View处理,此时调用的是View的dispatchTouchEvent(ev)方法。然后再在dispatchTouchEvent看是否执行onTouchEvnet方法。
好了至此ViewGroup的分发事件流程分析就算完了。
总结一下流程:
1:当事件序列到达此ViewGroup时,先清空标记,也就是上一个事件序列的标记。
2:再判断是否是ViewGroup是否拦截此事件,如果拦截该事件,就交给onInterceptTouchEvent(ev)。如果没有拦截,就继续向下分发。
3:此时如果时DOWN事件,那么就会循环执行View’Group的子View, 如果子View符合包含了触摸点并且此子View没有正在执行动画,那么递归判断此子View(可能是Viewgroup),寻找是否有消耗事件的子view,如果有,就更新消耗树。
4:除了DOWN事件,那么接下来的一系列手势,都会查看是否有消耗路径,如果有,那么就循环遍历该链表,分发接下来的一系列手势事件;如果没有,那么就把此ViewGroup当作普通的View处理,此时会调用父View的dispatchTouchEvent方法。
结论:总体来说,事件分发,也就是寻找消耗路径,并遍历消耗路径的过程。
View源码分析
当ViewGroup分发流程源码中,如果ViewGroup没有消耗树,那么就会把自己当成普通的View处理:
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
//注意此时传入的child是null。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
......
if (child == null) {
//调用父类的dispatchTouchEvent,而父类也就是View
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
......
}
通过ViewGroup中的源码,我们知道最终调用的还是View的dispatchTouchEvent, 所以接下来看下View的dispatchTouchEvent方法:
//核心代码:
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
上述代码是核心代码,先看下li对象,这是个ListenerInfo内部静态类,主要封装了View的功能性接口,比如:点击事件,长按事件等一系列接口对象。 比如我们设置点击事件,都是通过ListenerInfo对象调用的。从上述源码可以看出:首先判断是否设置了onTouchListener && onTouch返回了true,如果成立,那么result = true; 此时接下来的!result 也就是false,也就是说onTouchEvent方法执行不了。
因此:onTouchListener监听的方法onTouch返回值决定了是否能够执行onTouchEvent方法。
接下来看下onTouchEvent方法:
//是可点击的 || 可长点击的 || CONTEXT_CLICKABLE
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!post(mPerformClick)) {
//内部执行的还是onClick方法。
performClick();
}
break;
case MotionEvent.ACTION_DOWN:
......
break;
case MotionEvent.ACTION_CANCEL:
......
break;
case MotionEvent.ACTION_MOVE:
......
break;
}
return true;
}
return false;
如果是可点击的 || 可长点击的 || CONTEXT_CLICKABLE, 此时就消费了事件,反之就返回false,表示不消耗事件。 从上述代码可以看书 , performClick()是在Up的时候执行的,也就是说onClick方法是在Up的时候执行的。
当然前提是可点击的 || 可长点击的 || CONTEXT_CLICKABLE。
注意:
1:View的CLICKABLE和LONG_CLICKABLE都是false;TextView的CLICKABLE是false,LONG_CLICKABLE是true;Button的CLICKABLE和LONG_CLICKABLE都是true。
2:onClick调用的前提是,View接收到了DOWN和UP事件。
实际项目中的滑动冲突原理也就是:View的事件分发机制。
Android技术交流QQ群: