概述
在介绍点击事件规则之前,我们需要知道我们分析的是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是怎么处理事件的
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {return mDecor.superDispatchTouchEvent(event);}
复制代码
PhoneWindow直接把事件交给了DecorView,DecorView其实就是最顶层的View我们setContentView的View就是DecorView的一个子View,DecorView继承自FrameLayout,这个时候事件已经分发到了ViewGroup上
ViewGroup事件的分发
现在我们看一下ViewGroup的dispatchTouchEvent方法的源码
@Overridepublic 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时候才会遍历子viewif (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消费,就跳出循环,不再继续分发给其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget内部赋值mFirstTouchTarget=当前viewnewTouchTarget = 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之外的事件直接分发给目标viewTouchTarget 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时候才会遍历子viewif (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消费,就跳出循环,不再继续分发给其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget内部赋值mFirstTouchTarget=当前viewnewTouchTarget = 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消费,就跳出循环,不再继续分发给其他viewif (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {...//addTouchTarget内部赋值mFirstTouchTarget=当前viewnewTouchTarget = 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=nulltarget.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之外的事件直接分发给目标viewTouchTarget 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=nulltarget.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开发艺术探索
allenfeng.com/2017/02/22/…