之前根据网上的方法在搞listview加个侧滑菜单的时候会出现侧滑后listview无法滚动,虽然后续找到现成的方案解决了,但根本的问题所在:“事件分发机制”却没能深入了解。
安卓的事件分发机制其实是开发者必须知道的基础,但那会我跟大多数同学一样,只知道点击或者滑动事件给设置个监听,用onTouch,onClick去响应处理就行了,却并不清楚他们的关系,今天终于硬着头皮啃了啃源码,结合几位大神的教程,勉强缕清楚了事件分发的流程。
所谓时间,就是从用户手指点到屏幕到手指离开屏幕(包括中途的滑动)这个过程中的事件,事件类型主要分为四种:MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_CANCEL。按字面意思,就是按下,抬起,滑动,结束,最后一个非人为,可以放在一边,一般都是从DOWN到MOVE到UP,这些过程中的事件,就需要分发给对应的View来响应处理。
事件分发会遵从Activity —> ViewGroup —> View的顺序进行,主要由三个方法完成:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent(),分别是分发,拦截,处理点击。简单的理解就是,从Activity到ViewGroup到其子View进行分发,如果事件在过程中被拦截消费掉,那就通过onTouchEvent进行响应。
Activity分发的源码分析
那我们先从Activity分发开始,我们看Activity的dispatchTouchEvent() 源码
当事件为DOWN时(一般第一下都是DOWN),会执行 onUserInteraction()这个方法,通过查看这个方法发现是空的,后来请教大神知道这个方法是实现屏保功能,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法,我们可以忽略他。
那走到下面的if条件里,若getWindow().superDispatchTouchEvent(ev)这个方法返回true,那dispatchTouchEvent()就返回true,该点击事件停止往下传递 ,否则调用onTouchEvent(ev)这个方法消费事件(该情形发生在Window边界外的触摸事件)。
再看源码发现Window是个抽象类,查找资料发现其唯一实现类是PhoneWindow,所以此处getWindow()获取到的其实是PhoneWindow对象,其抽象方法superDispatchTouchEvent(ev)由子类PhoneWindow实现,不过Android Studio却并不能找到这个类,干脆直接去SDK路径(//sdk/sources/android-27/com/android/internal/policy/impl/PhoneWindow.java)下找到了它,我们看看PhoneWindow.superDispatchTouchEvent(ev)的源码
我们再看DecorView里的superDispatchTouchEvent(ev)方法
可见,DecorView里面调用了父类的dispatchTouchEvent(ev)方法。DecorView是PhoneWindow类的一个内部类,是顶层View及所有界面的父类,它继承FrameLayout,而FrameLayout继承ViewGroup,因此,ViewGroup间接等于DecorView的父类,最终,Activity的事件分发传递了ViewGroup的dispatchTouchEvent里,也就是分发到了ViewGroup。
ViewGroup分发的源码分析
从Activity源码里发现,分发走到了ViewGroup的dispatchTouchEvent,那我们看看ViewGroup的dispatchTouchEvent方法源码,由于源码巨长,我们看关键部分
这里disallowIntercept表示是否禁用事件拦截的功能(默认是false),通过requestDisallowInterceptTouchEvent方法可以设置为true,如果disallowIntercept为false即不禁用拦截功能,那会走进拦截方法onInterceptTouchEvent()里,给intercept赋值是否拦截,如果disallowIntercept为true,那intercept为true,即拦截事件。onInterceptTouchEvent()默认返回false,即默认intercept为dalse不拦截,当然我们也可以重写它进行拦截,则ViewGroup消费此事件。
intercept在方法后面的if语句中用到,太长了不贴了,当intercept为false不拦截时,会有一个for循环,遍历了当前ViewGroup下的所有子View,判断点击位置是否是该子 View 的布局区域,从而找到当前被点击的View。
找到子View后调用了dispatchTransformedTouchEvent(event, cancel, child,desiredPointerIdBits)方法,查看该方法源码
最终调用了child.dispatchTouchEvent(event),即走到了子View的dispatchTouchEvent分发方法里。
如果没找到子view,也调用了dispatchTransformedTouchEvent(event, cancel, child,desiredPointerIdBits),只不过这时候child值是null,查看源码知道在null时调用了它父类的dispatchTouchEvent(event),ViewGroup父类是View,最终还是走到了View这一步,只不过这个情况下ViewGroup是作为一个View将这个事件消费掉。
View分发的源码分析
最终分发流程走到了我们的View里面,我们查看View的dispatchTouchEvent(event)方法源码,由于5.0以上版本源码巨复杂,我们只看关键部分。
这里result是源码里是返回值,初始化为false。走到这个if判断里,有四个条件,我们逐个去看看是什么意思。
(1)li !=null && li.mOnTouchListener !=null
首先这个li,也就是mListenerInfo判断非空,我们看看他在哪里被赋值的,搜索一下发现是在getListenerInfo()这个方法里
那这个getListenerInfo()在进入这里之前是否被调用过呢,我们查一下它在哪被调用,很快就找到了熟悉的方法setOnTouchListener(),查看代码发现里面顺带把mListenerInfo的mOnTouchListener也给赋了值
也就是说,只要我们注册了Touch的事件,这个mListenerInfo跟mListenerInfo.mOnTouchListener就不为空,前两个判断为true。
(2)(mViewFlags &ENABLED_MASK) ==ENABLED
这个条件判断的是当前点击的控件是否为 enable,大部分View 都是 enable 的,所以这个基本为true
(3)li.mOnTouchListener.onTouch(this, event)
这里就回调了注册的Touch的事件的onTouch()方法,我们在设置OnTouchListener时会发现里面的onTouch()方法默认返回的false,因此会默认走到下一个if判断里,执行onTouchEvent(event)。当然,如果我们在onTouch()里改成返回ture,那此时dispatchTouchEvent()返回true,事件分发结束,不会走到后续流程。
接下来再看看onTouchEvent(event)这个方法源码执行了什么。
由于源码依旧巨长,我们还是只看关键部分
这个clickable变量后续会用到if判断,当它为true时时onTouchEvent(event)就会返回true,dispatchTouchEvent()返回true,停止分发,消耗掉这次事件。而clickable表示View 的 CLICKABLE 和 LONG_CLICKABLE 至少有一个为 true,即 View 可以被点击和长按点击,一般View 都满足。所以走到onTouchEvent(event)里这个事件就会被消费掉。
我们看看后面用到clickable的if判断
可以看到在ACTION_UP事件中,用到了performClickInternal()这个方法,该方法源码如下
到这里是不是很熟悉了!没错,我们走到了performClick()这里,可能有的同学还没明白,那我们再看看
performClick()源码
这里进入了同上面类似的判断,只要我们调用了setOnClickListener()注册了监听,里面的if条件就成立,最终,事件走到了click监听的onClick()方法里。这下明白了吧。这也说明了,onTouch()是优先于onClick()执行的,甚至可以在onTouch()里让onClick()不执行。
总结
捋了一长串过程,可能还是很模糊,那我们来总结下:
1.事件分发会遵从Activity —> ViewGroup —> View的顺序进行
2.事件分发的三个主要方法:dispatchTouchEvent()、onInterceptTouchEvent()和 onTouchEvent()
3.具体流程简写:Activity通过dispatchTouchEvent()传递到ViewGroup的dispatchTouchEvent(),若点击在Window外,则被Activity的onTOuchEvent消费事件;ViewGroup通过onInterceptTouchEvent()判断是否拦截,拦截则被ViewGruop的onTOuchEvent消费事件,不拦截则遍历到具体子View,传递给子View的dispatchTouchEvent(),如果没有找到子View,则ViewGruop作为View调用dispatchTouchEvent();View里面先调用onTouch(),onTouch()默认返回false,则继续走onTouchEvent(),onTouchEvent()里走到onClick(),onTouchEvent()默认返回true,停止分发,事件被消费掉。
4.onTouch()会优先于onClick()执行,如果将onTouch()返回true,则不执行onClick()。(可以用demo打log简单验证下)