概述
在介绍点击事件规则之前,我们需要知道我们分析的是MotionEvent,即点击事件,所谓的事件分发就是对MotionEvent事件的分发过程,即当一个MotionEvent生成以后,系统需要把这个事件传递给具体的View,而这个传递过程就是分发过程,MotionEvent我们上节已经介绍过
事件分发主要涉及以下几个方法:
- dispatchTouchEvent:用来进行事件的分发,如果事件可以传递到当前View那么此方法一定会被调用,返回结果受当前View的onTouchEvent和子View的dispatchTouchEvent方法影响,表示是否消耗当前事件
- onInterceptTouchEvent:在上个方法内部调用,用来判断是否拦截事件,如果当前View拦截了事件,那么在同一时间序列内,此方法不会再次被调用,返回结果表示是否拦截事件
- onTouchEvent:在dispatchTouchEvent方法中调用,用于事件的处理,返回值表示是否消耗事件,如果不消耗当前View无法再次接受到事件
这三个方法到底有什么关系?
我们先简述一下他们之间的关系,之后再进行源码的详细分析
当一个事件传递给一个根ViewGroup之后,这时他的dispatchTouchEvent就会被调用,进行事件的分发,如果该ViewGroup的onInterceptTouchEvent返回true,表示他要拦截此事件,接着这个事件就会交给ViewGroup处理,即他的onTouchEvent就会被调用,如果他的onInterceptTouchEvent返回fasle就表示不拦截此事件,这时就会把此事件传递给他的子View,接着子View的dispatchTouchEvent就会被调用,如此反复直到事件最终被处理
源码分析
当一个事件产生后,他的传递遵循如下顺序Activity→Window→View,即事件总是县传递给Activity,然后Activity传递给Window,最后Window传递给顶级View,顶级View接收到事件后,就会按照事件分发机制分发事件
Activity对事件的分发
当一个点击操作发生时,事件最先传递给当前的Activity,由Activity的dispatchTouchEvent进行分发,我们看下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的分发返回true,表示事件就此结束,返回false,表示没有人处理,那么Activity的onTouchEvent就会被调用
Window对事件的分发
那么Window是怎么分发事件的呢?我们看下Window的源码,我们发现Window其实是一个抽象类,superDispatchTouchEvent也是一个抽象方法
public abstract boolean superDispatchTouchEvent(MotionEvent event);
那么Window的实现类是什么?其实是PhoneWindow,那我们看一下PhoneWindow是怎么处理事件的
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow直接把事件交给了DecorView,DecorView其实就是最顶层的View我们setContentView的View就是DecorView的一个子View,DecorView继承自FrameLayout,这个时候事件已经分发到了ViewGroup上
ViewGroup事件的分发
现在我们看一下ViewGroup的dispatchTouchEvent方法的源码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//--------TAG=1-------------------这里是一开始---------------------------------------------------
//如果是Action_down 就对其先前所有的状态进行重置
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//--------TAG=2-----------------这里开始进行拦截验证-----------------------------------------------
//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就进行拦截验证
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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;
}
//------------------------------------------------------------------------------------------------------------------------------
....
//----------TAG=3----------------这里看是遍历子view---------------------------------------------------------------
//如果不拦截,并且不是cancel事件,就进行遍历子view分发事件
if (!canceled && !intercepted) {
...
//当ACTION_DOWN和ACTION_POINTER_DOWN和ACTION_HOVER_MOVE时候才会遍历子view
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//找到可以接受触摸事件孩子,从前向后遍历查找
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);
...
//判断触摸点是否在此View的范围中,是否在移动
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
//分发事件,如果事件被子view消费,就跳出循环,不再继续分发给其他view
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//addTouchTarget内部赋值mFirstTouchTarget=当前view
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
//-----------TAG=4-----------------这里已经遍历完了子view--------------------------------------------
// //遍历完所有的子View后,还没有处理事件,就自己处理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//Action_Down之外的事件直接分发给目标view
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;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
}
//------------------------------------------------------------------------------------------------------------------------------
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
首先我们分析一下拦截事件的源码
//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就进行拦截验证
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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;
}
这段代码我们可以看到,有俩种情况会判断是否要拦截当前事件,事件类型是Action_Down,或者mFirstTouchTarget != null,ACTION_DOWN我们可以理解,mFirstTouchTarget != null代表什么呢?
我们从后面的代码可以看出,事件由ViewGroup的子元素处理成功时,mFirstTouchTarget被赋值并指向该子元素,也就是说当ViewGroup不拦截事件交由子元素处理时mFirstTouchTarget != null
一旦ViewGroup拦截事件mFirstTouchTarget != null就不成立,而当ACTION_MOVE ,ACTION_UP到来时,由于(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)这个判断为false,ViewGroup的onInterceptTouchEvent不在会被调用,并且同一序列的其他事件,会默认交给ViewGroup处理
这里还有一种特殊情况,FLAG_DISALLOW_INTERCEPT标志位,这个标志位是通过requestDisallowInterceptTouchEvent来设置的,一般用于子View中,一旦FLAG_DISALLOW_INTERCEPT标志为被设置后,ViewGroup将无法拦截,除了ACTION_DOWN之外的其他事件,为什么要除了ACTION_DOWN呢,因为每当ACTION_DOWN带来都会重置FLAG_DISALLOW_INTERCEPT这个标记位,ACTION_DOWN事件总会调用自己的onInterceptTouchEvent询问是否拦截
强调一点requestDisallowInterceptTouchEvent,这个方法并不是万能的,执行他的前提是子View必须获取事件,假如父View的Down事件的onInterceptTouchEvent就返回true,拦截事件,那么子View做任何操作也不可能获取到事件
从上面分析我们可以得出结论
- 当ViewGroup决定拦截事件的时候,那么后续的点击事件将默认交给他,不再调用onInterceptTouchEvent
- FLAG_DISALLOW_INTERCEPT作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截Action_Down事件
- onInterceptTouchEvent不是每次都会调用的,如果我们要提前处理点击事件需要在dispatchTouchEvent
- 当我们遇到滑动冲突的时候,可以考虑FLAG_DISALLOW_INTERCEPT来处理
我们看一下ViewGroup不拦截的事件的情况
先看一下源码,这个是删减后的源码,看起来比较清楚
//如果不拦截,并且不是cancel事件,就进行遍历子view分发事件
if (!canceled && !intercepted) {
...
//当ACTION_DOWN和ACTION_POINTER_DOWN和ACTION_HOVER_MOVE时候才会遍历子view
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//找到可以接受触摸事件孩子,从前向后遍历查找
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);
...
//判断触摸点是否在此View的范围中,是否在移动
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
//分发事件,如果事件被子view消费,就跳出循环,不再继续分发给其他view
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//addTouchTarget内部赋值mFirstTouchTarget=当前view
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
首先遍历ViewGroup的所有子元素,然后判断判断子元素是否能接收到点击事件,是否能接收到点击事件主要由俩点来衡量
- 点击的坐标是否落在了子元素的区域内
- 子元素是否在播放动画
如果子元素满足这俩个条件,那么事件将传递给他处理,分发事件其实dispatchTransformedTouchEvent是这个方法做的,我们看一下dispatchTransformedTouchEvent源码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//先记住这一段判断cancel的源码,很重要下面分析
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
....
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
}
.....
return handled;
}
这里面主要代码如果 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) 为false,这个判断的意思是,如果不是ACTION_CANCEL,外部传入的cancel也为fasle,就进行下面的判断,而下面的判断主要是根据传入的child是否为null来判断的,如果child不为null,那么就调用child的dispatchTouchEvent方法,这个事件就交给子元素去处理,这就完成一轮的事件分发
如果child的dispatchTouchEvent返回为true,先不考虑事件怎么在子元素中分发,那么mFirstTouchTarget就被赋值,跳出for循环
//分发事件,如果事件被子view消费,就跳出循环,不再继续分发给其他view
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//addTouchTarget内部赋值mFirstTouchTarget=当前view
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
上面的代码完成了,给mFirstTouchTarget赋值,并且跳出for循环,终止对子元素的遍历,如果子元素的dispatchTouchEvent返回fasle,那么就会继续遍历子元素,把事件传递给下一个合适的子元素(如果还有合适的子元素的话)
mFirstTouchTarget赋值是在addTouchTarget方法内部完成的,mFirstTouchTarget是一个单链表结构
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//注意这里这里很重要,target.next =null,然后 mFirstTouchTarget = target;也就是说这时候的 mFirstTouchTarget.next=null
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍历所有的子元素事件都没有合适的处理,这里包含俩种情况,一种就是ViewGroup没有子元素,第二种就是子元素的dispatchTouchEvent返回了fasle,这俩种情况下ViewGroup会自己处理事件
//遍历完所有的子View后,还没有处理事件,就自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
注意这里child参数传入的是null,根据之前的分析就会调用 super.dispatchTouchEvent(event);由于ViewGroup也是继承自View,这里就会转到View的dispatchTouchEvent,即点击事件交给View处理
注意敲黑板了啊
我看了很多博客,都没有对这种情况进行分析,这个问题一度卡了我很久
现在考虑一种情况,如果父View的onInterceptTouchEvent的Down事件返回false不拦截,move up事件返回true拦截,这个效果就是子View只能收到Down事件而收不到Up和Move事件
那么我们现在分析一下这种情况,按照我们上方的分析,父View的Down事件不拦截,那么mFirstTouchTarget就会被赋值,第二次Move和Up事件要拦截,但是由于mFirstTouchTarget被赋值了,所以是走不到下面这步的
// //遍历完所有的子View后,还没有处理事件,就自己处理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
那么父View是怎么拦截Move和Up事件的呢?
当地一个Move事件传递给父View后,此时mFirstTouchTarget不为null,所以走拦截这一步代码
//如果是ACTION_DOWN,或者mFirstTouchTarget != null,就进行拦截验证
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
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;
}
拦截返回true后,不走遍历子Vew代码,直接到最后的判断代码
// //遍历完所有的子View后,还没有处理事件,就自己处理
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//Action_Down之外的事件直接分发给目标view
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;
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;
}
}
}
由于mFirstTouchTarget在Down的时候已经赋值不为null,会走下边代码
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
由于拦截事件,cancelChild为true,也就是说下面这个分发dispatchTransformedTouchEvent的方法传入的是true
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
在这个分发方法里,有判断Cancel事件的代码
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//先记住这一段判断cancel的源码,很重要下面分析
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
由于传入的cancel为true, 会重新定义事件为Cancel事件event.setAction(MotionEvent.ACTION_CANCEL);child不为null所以会调用child.dispatchTouchEvent(event);也就是说第一个Move事件,父View不会拦截,但会给子View发送一个Cancel事件
接下来会继续走代码
TouchTarget target = mFirstTouchTarget;
final TouchTarget next = target.next;
...
if (cancelChild) {
...
mFirstTouchTarget = next;
...
}
上面已经分析过cancelChild为true,进入方法给mFirstTouchTarget重新赋值mFirstTouchTarget.next,那么mFirstTouchTarget.next等于什么?看下面一段代码
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//注意这里这里很重要,target.next =null,然后 mFirstTouchTarget = target;也就是说这时候的 mFirstTouchTarget.next=null
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
其实mFirstTouchTarget.next=null,那整合起来就是把mFirstTouchTarget重新赋值为null,从这里开始,第二个Move事件就会直接传递给父View完成了拦截
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
总结
当父View不拦截Down事件,但要拦截Move和Up事件时,第一个Move事件会重新赋值为Cancel事件发送给子View,然后mFirstTouchTarget赋值为null,第二次开始的Move事件就会交给父View
View的事件分发源码
View对事件的处理比较简单,注意这里的View不包括ViewGroup,先看他的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
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;
}
}
...
return result;
}
View的时间传递比较简单,因为View(不包括ViewGroup),是一个单独的元素,无法向下传递事件,所以没有onInterceptTouchEvent方法,从上面源码可以看出
- 首先会判断与没有mOnTouchListener,如果有并且其中的onTouch方法返回true那么onTouchEvent放方法不会调用,可以看出mOnTouchListener的优先级高于onTouchEvent
下面看一下onTouchEvent方法的源码
首先看一下,当View处于不可用状态下,事件的处理过程
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
可以看出不可用的状态下,View消耗点击事件
再看一下对具体事件的处理
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 (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
....
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
break;
}
从上面代码为可以看出
- 只要View的CLICKABLE和LONG_CLICKABLE一个为true,不管他是不是DISABLED状态都消耗事件,只不过DISABLED不走下面的down,up事件
- 当Action_Up触发时,会调用PerformClick方法,如果View设置了onClickListener,那么PerformClick将调用他的onClick方法
- View的LONG_CLICKABLE默认是false,但是CLICKABLE是否为fasle,跟具体View有关,可点击的CLICKABLE为true,不可点击的CLICKABLE为false
- setClickable和setLongClickable可以改变CLICKABLE,和LONG_CLICKABLE的值
- setClickLinsterer和setLongClickLinsterer会自动设置CLICKABLE和LONG_CLICKABLE为true
到这里事件分发就处理完了
参考:Android开发艺术探索
http://allenfeng.com/2017/02/22/android-touch-event-transfer-mechanism/