Android事件分发机制及滑动冲突解决方案

Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效以及滑动冲突问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,总结一句:事件分发机制很重要

Android事件分发流程

网上关于事件分发机制的的博客很多很多,但是很多都是写个Demo然后贴一下输出的Log或者拿源码分析,然后一堆的注释和说明,读者可能很难读懂,或者是读懂之后,过不了多久便又忘记了。那么,今天我用一张图来总结一下Android整个事件分发机制的流程,如果你能在脑海里留下这张图, 记住分发机制的整个流程,再去阅读那些源码博客会不会更加的印象深刻呢!反正我是印象挺深刻的!好了,请看图!(自从记住了这张图,妈妈再也不用担心我被面试官虐啦!)

事件分发机制U形图

注释:

  • 1.整个流程图,分为三层:Activity,ViewGroup,View,即最简单的情况。
  • 2.整个事件从Activity开始,由Activity的dispatchTouchEvent做分发。
  • 3.虚线上的字代表了这个方法的返回值,分为false,true,super。
  • 4.目前图中所有事件是针对ACTION_DOWN的,对于ACTION_MOVEACTION_UP我们另行分析。
  • 5.View是没有onInterceptTouchEvent方法的,这个很容易理解,因为不会向下传递了,因此就没有是否拦截事件之说了。

结合整个图来看,我们得出事件流走向的几个结论(希望读者专心的对比U型图来记这些结论,多看几遍,脑子有比较清晰的概念。)

  • 结论1:返回值为super.xxx()的情况:事件的默认实现都是返回值为super.xxx(), 即我们没有对控件里面的方法进行重写或更改返回值,而是直接用super调用父类的默认实现,那么整个事件流向应该是从Activity---->ViewGroup--->View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View--->ViewGroup--->Activity从下往上调用onTouchEvent方法。若是ViewGroup则向下传递的时候会传给onInterceptTouchEvent再传给下层的dispatchTouchEvent,整个事件的流向是一个类U型图。

  • 结论2:返回值为false的情况:对于dispatchTouchEvent和onTouchEvent,除了Activity返回值为false代表自己消费该事件,ViewGroup和View都会将该事件回传给父控件的onTouchEvent来处理。而onInterceptTouchEvent返回值为false的时候代表不进行拦截,事件默认也是不拦截的,所以它和返回值为super.xxx()时是一样的,继续将事件传递给下层的dispatchTouchEvent来处理。

  • 结论3:返回值为true的情况:对于dispatchTouchEvent和onTouchEvent来说无论是Activity,还是ViewGroup和View返回值为true都代表自身来消费该事件,不再向下进行传递了。对于onInterceptTouchEvent来说,返回值为true代表拦截该事件的传递,既然拦截了,就代表不会往下传递了,这时候它会将事件传递给自身的onTouchEvent来处理。

以上这三个结论就代表了ACTION_DOWN事件的所有事件传递可能性,不知道读者对着U型流程图,有没有在头脑里有一个清晰的认识了呢。相信记住这三个结论之后,再去跟着源码理解,能更加对事件分发有深入的了解呢!OK,我们继续来看ACTION_MOVE和ACTION_UP是怎么传递的呢!

注意:上面讲解的都是针对ACTION_DOWN的事件,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,(case :ACTION_DOWN的返回值false,不是dispatchTouchEvent的返回值为false)后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。

上面提到过了,事件如果不被打断的话是会不断往下传到叶子层(View),然后不断回传到Activity,dispatchTouchEvent 和 onTouchEvent 可以通过return true 消费事件,终结事件传递,而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,ACTION_MOVE和ACTION_UP 会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到 ACTION_MOVE 等后续的事件的。(因为要消费事件,才有ACTION_DOWN和ACTION_MOVE 发生,因此只考虑返回值为true的情况)

对于ACTION_MOVE和ACTION_UP在不同函数中的传递,有以下结论:

  • 结论1:对于dispatchTouchEvent :返回值为true时,自己消费事件。因为返回值为true代表消费,事件不会往下面传,因此ACTION_DOWN事件传递到此处停止传递,ACTION_MOVE和ACTION_UP也传递到此处停止向下传递,这个时候传递方向是一致的。

  • 结论2:对于onTouchEvent :返回值为true时,自己消费事件。因为事件传递到onTouchEvent有可能是下层View或ViewGroup回传过来的,这时候ACTION_DOWN是经过下层传递回来的,但是此时ACTION_MOVE和ACTION_UP并不会传递到下层;也有可能是自身的onInterceptTouchEvent 返回了true传递过来的,这个时候ACTION_MOVE和ACTION_UP和ACTION_DOWN事件的传递流程也是一样的。

对于ACTION_MOVE、ACTION_UP终极总结:
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

滑动冲突解决方案

介绍完了事件分发机制的基本流程,我们来看看滑动冲突。滑动冲突的基本形式分为两种,其他复杂的滑动冲突都可以拆成这两种基本形式:

  • 1:外部滑动方向与内部方向不一致。
  • 2:外部方向与内部方向一致。

先来看第一种, 滑动方向不一致的情况。举个例子, 比如你用ViewPaper和Fragment搭配,而Fragment里往往是一个竖直滑动的ListView这种情况是就会产生滑动冲突,但是由于ViewPaper本身已经处理好了滑动冲突,所以我们无需考虑,不过若是换成ScrollView,我们就得自己处理滑动冲突了。图示如下:

滑动方向不一致的情况

再看看第二种,这种情况下,因为内部和外部滑动方向一致,系统会分不清你要滑动哪个部分,所以会要么只有一层能滑动,要么两层一起滑动得很卡顿。图示如下:

滑动方向一致的情况

对于这两种情况,我们有不同的方法来处理它。

第一种:第一种的冲突主要是一个横向的,一个竖向的,所以在开发中我们只要判断滑动方向是竖向还是横向的,再让对应的View滑动即可。判断的方法有很多,比如竖直距离与横向距离的大小比较,哪个距离大就判定为向哪个方向滑动的;滑动路径与水平形成的夹角等等。

第二种:对于这种情况,比较特殊,我们没有通用的规则,得根据业务逻辑来得出相应的处理规则。举个最常见的例子,ListView下拉刷新功能,需要ListView自身滑动实现滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动,这就涉及到滑动冲突问题了,如果不处理好滑动冲突,就会出现各种意想不到情况。对于这种情况的解决,我们可以采用拦截法:

  • 1.外部拦截法(由父容器决定事件的传递):让事件都经过父容器的拦截处理(onInterceptTouchEvent ),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法,其伪代码如下:
外部拦截法

代码注释:

a:首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器(onTouchEvent),子元素就没有机会处理事件了。

b:其次是up事件也返回了false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。

  • 2:内部拦截法(自己决定事件的传递):父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法,使用起来稍显麻烦,伪代码如下:

首先我们需要重写子元素的dispatchTouchEvent方法:

dispatchTouchEvent方法

然后修改父容器的onInterceptTouchEvent方法:

父容器的onInterceptTouchEvent方法

滑动冲突解决实战

    1. 滑动方向不一致的情况:

看代码看不出所以然,我们通过实例来看看滑动冲突是怎么样的。我们先模拟第一种场景,内外滑动方向不一致,我们先自定义一个父控件,让其可以左右滑动,类似于ViewPaper:

内外滑动方向不一致

然后在布局中添加listview

添加listview之后的界面

可以看到左右滑动确实失效了,说明确实产生了滑动冲突。那么我们就来解决一下吧!首先我们要明白滑动规则是什么,这个例子中如果我们竖直滑动就让ListView消耗事件进行滑动,水平滑动就让我们自定义的父容器滑动。

首先用外部拦截法,我们需要重写onInterceptTouchEvent方法,代码如下:

外部拦截法

这里我们判断横向滑动的距离与竖直滑动距离的长短。若是竖直滑动的长,则判断为竖直滑动,那么就是ListView的滑动,就将intercepted置为false,让父容器不拦截,交由子元素ListView处理。若是横向,则intercepted置为true,交由父容器处理。OK,完美解决滑动冲突问题,效果图:

最终效果图

接下来看看内部拦截法:重写其dispatchTouchEvent方法:

dispatchTouchEvent方法

再重写外部父容器的oninterceptTouchEvent方法:

父容器的oninterceptTouchEvent方法
  • 2.滑动方向一致的情况:

接下来看看同方向的滑动冲突,这里我们用一个竖直的ScrollView嵌套一个ListView做例子。首先看看没有解决滑动冲突的时候是咋样的:

滑动方向一致的情况

我们看到只要ScrollView可以滑动,内部的ListView是不能滑动的。那我们现在来解决这个问题,同向滑动冲突和与不同向滑动冲突不一样,得根据实际的需求来确定拦截的规则

这里我们的需求是当ListView滑到顶部了,并且继续向下滑就让ScrollView拦截掉;当ListView滑到底部了,并且继续向下滑,就让ScrollView拦截掉,其余时候都交给ListView自身处理事件。

首先用外部拦截法,我们需要重写ScrollView的onInterceptTouchEvent方法,代码如下:

外部拦截法

这里我们看到Down事件里我们并没有返回false而是返回super.onInterceptTouchEvent(event),这是因为ScrollView在Down方法时需要初始化一些参数如果我们直接返回false,会导致滑动出现问题。并且前面说过ViewGroup的onInterceptTouchEvent方法是默认返回false的,所以我们这里要返回super方法才可。OK,完美解决,效果图就不贴出来了,你懂的。

接下来看看内部拦截法:先重写ScrollView的onInterceptTouchEvent方法,让其拦截除了Down事件以外的其他方法:

父容器的onInterceptTouchEvent方法

在重写ListView的dispatchTouchEvent方法,规则已经说明过了:

listview的dispatchTouchEvent方法

效果图:

最终效果图

最终实现了完美解决滑动冲突。解决问题的感觉是不是特别爽呢! <{=....(嘎嘎~)

好了,这篇文章到此结束,希望各位读者看完之后能对事件分发机制有更深入的了解,在实际项目开发中,遇到滑动冲突问题时能够轻松解决问题,喜欢的话点个赞吧。(#.#)

推荐阅读更多精彩内容