Android View的事件体系(四)View的事件分发机制

View的事件分发机制


点击事件的传递规则

我们这里要分析的对象就是MotionEvent,即点击事件,所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程,点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent.

8656692-ce920631cb9e9a44.png

用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件

8656692-8f4851d9e3afc8ff.png

在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件

8656692-5b196501e5920c26.png

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列,当前View无法再次接受到事件

三个函数之间的关系用如下伪代码表示

8656692-2dafbc1f5646c0d4.png

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理.

当一个View需要处理事件时,如果它设置了onTouchListener ,那么onTouchListener中的onTouch方法会被回调,这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法将不会被调用,由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent要高,在onTouchEvent方法中,如果当前设置的有OnClickListener ,那么它的onClick方法会被调用,可以看出,我们常用的OnClickListener,其优先级最低.

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View.顶级View接收到事件后,就会按照事件分发机制去分发事件,考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件最终传递给Activity处理,即Activity的onTouchEvent会被调用.

事件传递机制总结

1 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束

2 正常情况下,一个事件序列只能被一个View拦截且消耗,因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件就会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊可以做到,比如一个本该由自己处理的事件,通过onTouchEvent强行传给其他View处理

3 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了

4 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false)那么同一个事件序列中的其他事件都不会再交给它来处理,并且事件将重新交给它的父元素去处理,即父元素的onTouchEvent会被调用,意思是事件一旦重新交给它的父元素去处理,那么它就必须消耗掉,否则同一个事件序列中剩下的事件就不会再交给它来处理了.

5 如果View不消除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理

6 ViewGroup默认不拦截任何事件,Android源码中ViewGroup的onInterceptTouchEvent方法默认返回false

7 View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用

8 View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的.

9 View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者longClicjable有一个为true,那么它的onTouchEvent就返回true

10 onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件

11 事件的传递是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent,方法可以再子元素中干预父元素的事件分发过程,但ACTION_DOWN事件除外

事件分发机制源码解析


Activity对点击事件的分发过程

点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行派发,具体的工作是由Activity内部的Window来完成的,Window会将事件传递给decor view ,decor view一般就是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得,我们先从Activity的dispatchTouchEvent开始分析

8656692-0dfd2dcb660b657e.png

首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用

Window是如何将事件传递给ViewGroup的?,Window是个抽象类,而Window的superDispatchTouchEvent方法也是个抽象方法,因此我们必须找到Window的实现类才行

8656692-279564c608a2f147.png

Window类可以控制顶级View的外观和行为策略,它的唯一实现位于android.polocy.PhoneWindow中,当你要实例化这个Window类的时候,你并不知道它的细节,因为这个类会被重构,只要一个工厂方法可以使用,尽管着看起来有点模糊,不过我们可以看一下andorid.policy.PhoneWindow这个类,尽管实例化的时候此类会被重构但功能是类似的

由于Window的唯一实现是PhoneWindow,看一下PhoneWindow是如何处理点击事件的

8656692-3c8fd3701c6c8f0b.png

PhoneWindow将事件直接传递给了DecorView.

8656692-3a69334a09193e0a.png

目前事件传递到了DecorView里,由于DecorView继承自FrameLayout且是父View,所以最终事件会传递给View.

顶级View对点击事件的分发过程

点击事件到达顶级View以后,会调用ViewGroup的dispatchTouchEvent方法,然后的逻辑是这样的:如果顶级ViewGroup拦截事件即onInterceptTouchEvent返回false,则事件由ViewGroup处理,这时如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用,也就是说,如果都设置的话,onTouch会屏蔽掉onTouchEvent.在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用,如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用,到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的,如此循环,完成整个事件的分发.
首先看ViewGroup对点击事件的分发过程,其主要实现在ViewGroup的dispatchTouchEvent方法中,这个方法比较长,这里分段说明.

8656692-034f9859642c2fd1.png
View是否拦截点击事件

从上面代码我们可以看出,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null,ACTION_DOWN事件好理解,那么mFirsrTouchTarget!=null是什么意思呢?这个从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget!=null反过来,一旦事件由当前ViewGriup拦截时,mFirstTouchTarget!=null就不成立,那么当ACTION_DOWN和ACTION_UP事件到来时,由于(actionMasked==MotionEvent.ACTION_DOWN||mFirstTouchTarget!=null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会在被调用,并且同一序列中的其他事件都会默认交给它处理

这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPY标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中,FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,因为ViewGriup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效,因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件.

在下面的代码中,ViewGroup会在ACTION_DOWN事件到来时做重置状态的操作,而在resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT进行重置,因此子View调用request-DisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理

8656692-56f2fb28103650f6.png

当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法,FLAG_DISALLOW_INTERCEPT这个标志的作用是让ViewGroup不再拦截事件,当然前提是ViewGroup不拦截ACTION_DOWN事件,总结起来有两点:

1 onInterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent 方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递给当前的ViewGroup

2 FLAG_DISALLOW_INTERCEPT标记位的作用给我们提供一个思路,当面对滑动冲突时,我们可以是不是考虑用这种方法去解决问题

当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理

8656692-cf3489c7ac9273c5.png

首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接受到点击事件,是否能够接受点击事件主要由两点来衡量,子元素是否在播放动画和点击事件的坐标是否坐落在子元素的区域内,如果某个子元素满足这两个条件,那么事件就会传递给它来处理,dispatchTransformaedTouchEvent实际上调用的就说子元素的dispatchTouchEvent方法,在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发

8656692-7770d24a9fd78f47.png

如果子元素的dispatchTouchEvent返回true,那么mFirstTouchtTarget就会被赋值同时跳出for循环

8656692-1d6fb721c2842156.png

这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历,如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素的话)

其实mFiestTouchEvent真正的赋值过程是在addTouchTarget内部完成的,从下面的addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构.mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件

8656692-7b90f47ee777294c.png

如果遍历所有的子元素后事件都没有被合适的处理,这包含两种情况:第一种是ViewGroup没有子元素,第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false,在这两种情况下,ViewGroup会自己处理点击事件

8656692-c0cffe44684e68d2.png

这里就转到了View的dispatchTouchEvent方法,即点击事件交由View来处理

View对点击事件的处理过程

8656692-9751638d0d06caec.png

View对点击事件的处理比较简单,因为View是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件,从上面的源码可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件

接着在分析onTouchEvent的实现,先看当View处于不可用状态下点击事件的处理过程,很显然,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用

8656692-b9e0b0cd15b64617.png

接着,如果View设置有代理,那么还会执行TouchEvent的onTouchEvent方法,这个onTouchEvent的工作机制看起来和OnTouchListener.

8656692-c5c400844e6519c5.png

下面看一下onTouchEvent中对点击事件的具体处理

8656692-b50b63039d1c6eba.png

从上面的代码来看,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态,当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法

8656692-a473c22bea1716c5.png

View的LONG_CLICKABLE熟悉默认为false,而CLICKABLE属性是否为false和具体的View有关,确切来说是可点击的View其CLICKABLE为true,不可点击的View其CLICKABLE为false,比如Button时可点击的,TextView是不可点击的,通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性,另外,setOnClickable会自动将View的CLICKABLE设为true,setOnLongClickListenr则会自动将View的LONG_CLICKABLE设为true

8656692-6b36d791734e653f.png
8656692-a777aaf490cb8e7b.png

猜你喜欢

转载自blog.csdn.net/weixin_33973600/article/details/87218685