概述
一直以来都想整理一下事件分发方面的东西,做了这么久的安卓开发,写了很多的控件,梳理一下知识,往后就不回踩坑了。刚好最近需求业务忙,界面写了很多,处理了一些滑动冲突和自定义View事件View的分发方面的事情,将心得记录一下吧。
事件分发原理
先从原理说起吧,网上有很多图,找了一张画的比较清晰的,错误不是很多的,放上来,也算是”站在巨人的肩上了“。

我今天也不打算照着图在这里念,相信大家对事件有一定理解的话,都是看的懂的。
准备工作
做一些实验总的来说,才能更好的理解,本文也不打算从源码着手,如果想了解这方面,我觉得《安卓开发艺术探索》这本书讲的不错;同时我也不打算改一个参数,贴一下结果,这样的话,废话太多,那么我想比较好的一种方式是,先列出我做实验的最初的代码,然后,直接写出我的结果,根据结果,我们会过去改参数来验证,好了,我就是这么打算的。
虽然一开始上来就贴代码很操蛋,但是我还是决定来一发。
首先准备三个类,分别是Activity,Container和SubView,首先贴Activity的代码。
然后是Container父容器类。
SubView类
一个打印事件类型的类:
将他们放入一个页面,像这样。

好了,以上就是我的准备工作,我们不断的在这七个方法中,不断的改变返回值,去掉或者保留super方法,观察结果,最后的到下面的一些结论。
Activity的事件
Activity的事件主要包括dispatchTouchEvent和onTouchEvent事件,是DecorView以下整个事件传递链条的起点
Activity的dispatchTouchEvent
Activity的dispatchTouchEvent主要是进行事件分发,对于Activity来说,这个事件和ViewGroup的dispatchTouchEvent事件不太一样,在ViewGroup中,如果ACTION_DOWN的时候返回了false,事件就不会再到来了,但是对于Activity,无论返回true或者false,事件继续到来,所以其返回值并没有任何意义,也就是说无论如何,Activity都会收到事件,但是值得注意的是其super.dispatchTouchEvent,它才是派发事件的真正执行者,只要执行了它,事件才会往下传。同时Activity的onTouchEvent能够收到事件,和dispatchEvent唯一相关联的就是,如果Activity调用super.dispatchTouchEvent将事件传下去了,才有可能发生所有的子View或ViewGroup都不处理,而捡漏捡到的事件处理,才会触发Activity的onTouchEvent事件,onTouchEvent事件是整个我们平时普通开发里面的最后一环。
Activity的onTouchEvent
Activity的onTouchEvent是整个事件流的回溯的最后一环,也就是Activity将事件分发出去后,如果大家都不要的情况下,就交给Activity的onTouchEvent处理,这个很好理解。
ViewGroup的事件
ViewGroup能够正常工作需要dispatchTouchEvent、onInterceptEvent和onTouchEvent正确的处理。
ViewGroup的dispatchTouchEvent
对于ViewGroup,dispatchTouchEvent()的返回值true和false主要产生的影响是针对ACTION_DOWN,对于ACTION_DOWN事件,如果返回了false,那么下一次事件就不会再走到这里了,如果返回了true,后续的事件都会传到这里,对于其他事件,无所谓,返回true或者false,对于ViewGroup的diapatch来说,没有影响。对于ViewGroup的子View来说,他们关心的是是否ViewGroup调用了super.dispatchTouchEvent,因为这个操作才是起到将事件往后传递的作用的。如果我们不进行干涉,默认的ViewGroup的super.dispatchTouchEvent的返回值受到onInterceptEvent和onTouchEvent返回值的影响,如果说onInterceptEvent返回true进行了拦截,那么主要看onTouchEvent的返回值了,即告诉ViewGroup我们自己是否需要消费事件,如果说要消费即ACTION_DOWN的时候返回了true,那么后续的event才能继续到来进行处理,如果说ACTION_DOWN返回false,意思就是说我自己没什么好处理的,那么事件就不要来我这里了。另一种情况是如果在ACTION_DOWN的时候,我们onInterceptEvent返回了false,即不进行拦截,那么我们就得看子View或者ViewGroup们的需求啦,如果说它们都没有设置事件也没有处理事件的需求,即原装不动的情况下,那么我们的super.dispatchTouchEvent会返回false,这样一来,既然后面的子View们都不需要事件,同时也不拦截事件给自己,那么后续的事件也不要到来好了。
ViewGroup的onInterceptEvent
ViewGroup的onInterceptEvent事件表示是否要对dispatchTouchEvent分发来的事件进行拦截,默认是不会去拦截的,也就是说都要去走到子View或ViewGroup中去,当然,我们可以根据业务需求去做处理,比如滑动冲突的时候什么的。当onInterceptEvent拦截的时候,子View或ViewGroup就不会再收获到dispatchTouchEvent派发的事件了,而交由自己的onTouchEvent去处理,如果自己的onTouchEvent将事件消费了,那么到此结束,如果没有,那么事件继续回溯。如果说ViewGroup的onInterceptEvent使用的是super.onIntercept的返回值,那么,情况得根据子View或ViewGroup的返回情况,一般情况下,如果子View或ViewGroup对事件是由需求的话,super.onInterceptEvent都会返回false,表示子View或ViewGroup们消费了事件,那么父亲你就别再处理啦。默认没有需要消费事件的子View或着ViewGroup的时候,super.onInterceptEvent会返回true,父亲经过计算告诉自己,孩子们都没有需求,那事件就我来处理吧。但是比如说子View或ViewGroup是由消费事件需求的,比如ListView,ScrollView或者带点击滑动事件侦听的View或ViewGroup等,需要根据事件进行业务需求,那么这个时候,父亲通过计算方法会告诉自己,我这个时候不能去拦截,把事件交给孩子们。但有些时候,现有的ViewGroup的嵌套的时候,一个作为另一个的父亲,两者都需要对事件进行业务需求,从而消费了事件,那么,滑动冲突就发生了,这个时候需要我们进行相应的改造处理,才能让他们融洽的工作在一起。
ViewGroup的onTouchEvent
ViewGroup的onTouchEvent事件是处理拦截到自己地方的事件,这个方法相对来说就不难理解了,如果事件陆陆续续的到来,在这里实现相关的业务需求就行啦。
View的事件
View的事件是树的叶子节点,如果树分叉,那么就是每个叉的最后一个节点,是分发链的最后一环。它的事件的正常处理依赖dispatchTouchEvent和onTouchEvent两个函数的正常工作。
View的dispatchTouchEvent
作为树的最后一环,它没有子节点。首先来看super.dispatchTouchEvent的返回值,其实它的派发事件,唯一的意义就是其返回值,在ACTION_DOWN的时候,告诉父亲要不要继续让事件继续传到这里,而在事件分发方面,它虽然已经没有了子View或者ViewGroup,但是,这里的super.dispatchTouchEvent的意义在于将事件要不要传给onTouchEvent,如果不执行super.dispatchTouchEvent,直接返回一个true或者false,那么onTouchEvent都是不会被触发的,在默认值方面,super.onTouchEvent会根据当前View是否有事件消费需求,包括onTouchEvent的返回值,OnClick事件或者其他事件消费等,计算出自己是否需要让父容器继续将事件传过来,如果没有消费需求,那么super.dispatchTouchEvent返回false高速父容器就不要传事件过来了,如果需要,那么super.dispatchTouchEvent会返回true,让事件继续传递过来。
View的onTouchEvent
View的onTouchEvent主要是处理分发到该View的事件,它的返回值会影响到super.dispatchTouchEvent的返回值,返回true表示消费了事件,返回false表示不对事件进行消费,这个和ViewGroup类似。
学以致用
上面是我通过试验得到的我自己的心得,它也帮我在实际开发中起到了指导作用,值得提到的事两个事情,分别是滑动冲突和自定义下拉刷新包裹容器PullToRefreshWraper;
滑动冲突
有一次一个需求是在横向滚动的HorizontalScrollView里面,包裹多个竖向滚动的ListView,别问我为什么有这个需求,我特么怎么知道产品会给我提这种需求。那么问题来了,套在一起不能用,ListView直接残废了,不能滚动,但是明显我们的需求是要竖向滚动的,这不符合需求,好吧,查StackOverFlow吧,解决方法一万种,最后,通过参考了《Android开发艺术探索》一书,通过梳理了一下事件分发机制,合理的去解决的冲突。解决的办法如下,核心思路是:当我们计算出滑动轨迹是偏横向的时候,让父容器拦截事件,在轨迹偏竖向的时候,让父容器返回false,不去拦截事件,那么,但是要注意,为了保证子ViewGroup即ListView能收到后续的事件,要保证ACTION_DOWN的事件能传到ListView,所以在ACTION_DOWN的时候,要将拦截的返回值设置为false,在ACTION_UP的时候,其实父容器和子容器都不是很需要这个事件,我们也就无需关心了,我们在这里返回false,最重要的是在ACTION_MOVE的时候,确定将事件是给到HorizontalScrollView还是竖向的ListView,我们得按逻辑进行判断,这样,大家都能够愉快的工作了。
自定义下拉刷新包裹容器PullToRefreshWraper
我们的需求是这样子的,我会包裹一个子View,只能是一个View,它会在它滚动后告诉我它的状态,滚动到顶了还是滚动到底了,如果是滚动到定了,如果父容器及Wraper接收到向下拉的事件,那么让父容器来处理,父容器会根据计算的距离scrollY,不断的去更新一个值observedY,比如我的observedY计算方法是取抛物线,随着拉动距离越大,但是增量减小,最终都不会到极限值,那么无论拉多久,都拉不到底,这种情况下,在被包裹子View内容滑动到顶的时候,根据observedY可以将子View通过layout方法放置在父容器的距离父容器顶端observedY距离的地方。并且更具observedY去更新下拉头的状态,思路就是这样子。
以上的代码片段中,值得注意的是dispatchTouchEvent的书写,其中处理逻辑的地方按照以上的思路就不说了,看最后两行,并没有直接返回super.dispatchTouchEvent(ev),而是每次都调用super.dispatchTouchEvent(ev),但是返回都是true,意思就是说,事件都要到我这儿来,并且事件都要分发给子View,其中子View拿着我们给的事件去滚动,当滚动到不能滚动的时候,我们父容器获取到它的状态,进行相应的处理就行啦。代码容易理解,就不赘述啦,但是以上的代码中有很明显的缺陷,那就是多指头下拉怎么办呢?这个问题放在下一个小部分。
多指下拉过滤
我们看到很多好的App作品都是支持多指下拉的,其实多指下拉很简单,我们一开始担心的是从一个点到另一个点,那么y会回到原来的值,我们用y来做判断就又导致observedY归零了,那么表现出来就是滚动的跳变,这在交互上很不友好,那么为了解决这个问题,我们需要过滤多指下拉,从以上的代码中我们可以看出我们使用的observedY是累积量,那么从这里入手,我们将区别出来的另一只手指放上来的时候,的那个跳变值过滤掉,直接使用后续的累加量不就行了吗?事实证明的却如此,增加一个变量mEvents标志位,最后代码如下。
总结
纸上得来终觉浅,绝知此事要躬行,讲了这么多,自己写两个demo理解会深入很多,但看别人踩过的坑,下次注意一下,那么会好很多,希望这篇文章对大家有帮助。