自定义View(二)--表层浅析View的事件分发机制和滑动冲突
不诗意的女程序猿不是好厨师~
    这篇文章来得有些曲折,因为事件分发机制一直以来都是Android界的一个重难点。很多的初学者甚至中级开发者面对这个问题都还会困惑不已。当然,我肯定是属于“初学者和中级开发者”行列的,所以,我到现在也并没有完完全全的掌握所有的事件分发机制。什么?!那我还敢写?!是的,我敢写,因为虽然我没有100%掌握,但是我掌握的部分还是足以解决大多数问题的。所以,还是天不怕地不怕的把我所知的给整理了一下。讲得不对的地方,还请前辈们指出,我再改,我一直认为“学习就是一个不断与错误磨合的过程”。
    注:本文实际创建于2017/5/1。原来的文章已经挪至自定义View(三)—自定义View整个流程的梳理与总结。
在上一篇的文章《自定义View(一)–View的基础概念,工作流程及生命周期的理解》中,我也提到了事件机制,那时讲得比较粗略,今天就让我们来更加全面地做一下总结。
View的事件分发机制
Android的事件分发机制会令很多人头大,它确实不易,但也没有传说中的那么冷酷无情。所以思考再三,我还是在标题上加了“表层浅析”这些字眼,以表示我对事件机制的敬畏之情。
本篇我也只对事件分发机制做简单的概括性介绍,虽简单,但却不失实用性。因为在具体的工作中有了这些基本而实用的东西,我们就有了顺利完成任务的筹码。这里我主要以点击事件的分发为主体来讲。
首先明确一下我们要分析的对象——MotionEvent,即点击事件。
还记得点击事件的分发过程涉及哪些重要的方法吗?
是的主要有三个:dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent。
下面让我们来一一介绍:
①public boolean dispatchTouchEvent(MotionEvent event)
从方法名便可以知道它的作用就是:进行事件的分发。
只要事件能够传递给当前的View,那么该方法就一定会被调用,返回的结果受当前View的onTouchEvent和下级View的dispathchEvent方法的影响,返回为true,表示消费当前事件;返回为false,则表示不消费当前事件。
②public boolean onInterceptTouchEvent(MotionEvent event)
这个方法又顾名思义:它是用来判断是否拦截某个事件的,该方法会在dispatchTouchEvent方法的内部被调用。如果当前的View拦截了某个事件,那么在同一个事件序列当中,此方法就不会被再次调用了。返回true,表示拦截当前事件;返回false,则表示不拦截。
③public boolean onTouchEvent(MotionEvent event)
也是在dispatchTouchEvent方法中被调用,是用来处理点击事件的,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
说了这么多你是不是有点迷糊了?哎呦我去,话说,他们三个到底是什么关系呢?怎么感觉比三角恋还虐心呢?阁下稍安勿躁,在《Android开发艺术探索》一书中,有一段伪代码可以很好的表明这三者的关系,您请看:
public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume=false;
        if(onInterceptTouchEvent(ev)) {
            consume=onTouchEvent(ev);
        }else{
            consume= view.dispatchTouchEvent(ev);
        }
        return consume;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
怎么样,看了这段代码是不是有一种相见恨晚,柳暗花明的感觉,再画个更直观的图解释一下:
这里写图片描述
到这,是不是思路清晰多了。
顺带说一句,这里我们还要注意一下事件传递的优先级问题:
①如果我们给View设置了OnTouchListener,那么它的优先级会比onTouchEvent高。
②平时我们使用最频繁的OnClickListener,它的优先级最低,处于事件传递的尾端。
事件的分发还有一些有趣的“原则”,不知道你发现了没有?
①“专一”原则:
一般情况下,一个事件序列只能被一个View拦截且消耗。也就是说只要事件序列可以传递给这个View且它进行了拦截,那么这个事件序列内的所有事件都要交给它来处理,因此同一个事件序列不能分别被两个View同时处理。当然,也会有特例,如果通过特殊手段的话,这个我们很少见。所以总体来说,一个事件序列还是比较“专一”的,只”真爱”一个View。
②“霸道总裁”原则:
这个和①其实是很类似的,只是考虑的角度略有不同。是这样的,某个View一旦开始处理事件,如果他不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再交给它处理,且事件会重新交个它的的付群去处理。是的事件序列就这么“任性”,事件一旦交给一个View处理,那这个View就必须全部接受消耗掉,否则都不给你,追求一个“全”。
讲了这么多,按照惯例我们还是画个图来总结一下,事件传递的过程吧:
这里写图片描述
一句话概括就是:
分发过程是由外到内的,消费的过程是由内到外的。
即:事件总是先传递给Activity,Activity再传递给顶级View,顶级View接收到事件后会按照分发机制去继续向内分发,这时候我们需考虑,如果一个子View的onTouchEvent返回false,那么它的父容器的onTouchEvent会被调用,以此类推,如果所有的元素都不处理这个事件,那么这个事件最终将会回传给Activity,即Activity的onTouchEvent方法会被调用。
用那个大家都知道的比喻来说就是:它就好比领导向下级分配任务,分配到一个小员工手里,结果这个小员工解决不了(onTouchEvent返回了false),那就只能将这个任务交给水平稍高些的上级(小员工上级的onTouchEvent被调用),如果这个上级还解决不了,那就继续一层层的将任务往上抛,实在不行那就只能由大boss亲自上阵了。
当然以上这些都只是一些理论上的结论,就好比一些数学公式一样,要想更细致的理解它的“推导过程”,那还是建议去研读源码吧。
View的事件分发就先暂且讲到这里,下面在让我们来看看View中常见的一些滑动冲突吧。
View的滑动冲突
什么情况下容易产生滑动冲突
有没有这种体验,自己写了一个布局,内外两层都能滑动,怎么有时候里面那层就不能动了?怎么我是想让它爸爸动的,可是为什么却是它儿子响应了?怎么他们动起来就这么卡顿?如果没猜错的话,你很可能是遇到滑动冲突了。
所以说,如果你的界面中内外两层同时都可以滑动,那滑动冲突就会很容易产生。
常见的滑动场景有哪些
常见的滑动场景概括一下来说也就三种:
    ①—-里外的滑动方向不一致,如图2.1
    ②—-里外的滑动方向一致,如图2.2
    ③—–①和②的嵌套结合,如图2.3
    这里写图片描述
先初步分析一下该怎么处理
    要解决①这种滑动冲突,那我们肯定要先判断了,如果是左右滑动,那就让外部的View拦截点击事件,如果是上下滑动,那就让内部的View拦截点击事件。
    对于②这种滑动冲突,①的方法肯定就不管用了,那我们就要另谋出路了,这个时候我们就应该从业务上找突破点了,比如业务上有规定:当处于某种状态是,需要外部View来相应滑动,而处于另一种状态时则需要内部View来响应。那么这个时候我们就要根据这种业务需求来给出响应的处理规则了。
    这么说是不是太抽象了,那就拿个具体的例子来说吧(PS:这张图是从网上找的,自己的项目中也用到了这个效果,但是由于产品还未上线,不方便提前展示。)
    这里写图片描述
    外面是一个可以上下滚动的StickyLayout,里面放了一个Header和ListView,内外两层都可上下滑动,于是,滑动冲突也就出现了。解决这个问题,我们就要从它的业务规则入手了,它的规则是:当Header显示时或者ListView滑动到顶部时,由外部的StickyLayout拦截事件;当Header隐藏时,又要分情况,如果ListView已滑动到顶部且当前手势是向下的,继续由外部StickyLayout拦截;如果其他情况则由ListView进行拦截。这样我们就大致知道该怎么做了。
    ③的滑动冲突感觉就更麻烦了。这个不能口说无凭,具体的印象很重要,还是来个图吧。
    这里写图片描述
    外部是一个可以右滑退出的控件(可以左右滑动),内部有一个可以左右滑动的(ViewPager+TabLayout),每个ViewPager里又是可以上下滑动的ScrollerView。这种冲突就很复杂了, 怎么办呢?其实它的解决主要需要耐心,如果你已经掌握了①②,那么这个③也就没有想象中的那么难了,解决办法无非是拆分,拆成一层一层的,把它转换成单个的①和②,然后再逐个击破。
接着总结一下滑动冲突的解决办法
其实这个很干粹了,真的要说也就两种:外部拦截法,内部拦截法。
    外部拦截法
    何为外部拦截法?它其实说的就是:点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。再通俗点就是,你是爸爸你说了算,点击事件都先经过你。
    外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法内部做相应的处理。
    还是来段伪代码吧,这样更加容易懂,也更加有feel:
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted=false;
        int x= (int) ev.getX();
        int y= (int) ev.getY();
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercepted=false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(父容器需要当前点击事件) {
                    intercepted=true;
                }else {
                    intercepted=false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted=false;
                break;
            default:
                break;
        }
        mLastXIntercept=x;
        mLastYIntercept=y;
        return intercepted;
}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
还是来分析一下上面的伪代码。上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只要改一下“父容器需要当前点击事件”这个条件即可,其他不需修改也不能修改。为什么会这样呢?因为在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截DOWN事件,这就涉及到我上面所说的“霸道总裁”原则了,一旦父容器拦截了DOWN,那后续的MOVE和UP事件都会交由父容器来处理,这样一来你就不是在解决滑动冲突,而是赤裸裸的专制啊,事件压根就传不到子元素,跟子元素就没有关系了呀。其次是MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则就返回false。最后是UP事件,这里必须返回false。它本身是没有多大意义的,比如一旦父容器决定在MOVE时进行拦截了,那UP肯定也会属于父容器统治了。
    内部拦截法
    那么问题又来了什么是内部拦截法呢?内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消费掉,否则再交由父容器进行处理。好吧,这下终于轮到听儿子的了。内部拦截法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。这次我们要重写子元素的dispatchTouchEvent方法。
    来,上伪代码:
@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x= (int) ev.getX();
        int y= (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN :
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX=x-mLastX;
                int deltaY=y-mLastY;
                if(父容器需要此类点击事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX=x;
        mLastY=y;
        return super.dispatchTouchEvent(ev);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
照旧来分析下伪代码:
上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能改动。这又是为什么呢?因为除了子元素需要做处理以外,父元素也要默认拦截除了DOWN以外的其他事件,只有这样,当子元素调用
parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。
这里需要注意一点,父容器是不能拦截DOWN事件,因为,又是上面所说的“霸道总裁”原则,一旦父容器拦截了DOWN,那么所有的事件都无法再传递到子元素中去了,这样内部拦截就无法起作用了。
所以父元素还要做如下的修改:
@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action=ev.getAction();
        if(action==MotionEvent.ACTION_DOWN) {
            return false;
        }else{
            return true;
        }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
好了,到这里本文就结束了。在看了本文后,不知道你对于View的事件分发机制和滑动冲突有没有一些新的感悟呢~
--------------------- 
作者:李诗雨 
来源:CSDN 
原文:https://blog.csdn.net/cjm2484836553/article/details/54387722 
版权声明:本文为博主原创文章,转载请附上博文链接!
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号