View滑动冲突

View和ViewGroup的事件分发机制

概述

  1. 所谓点击事件的事件分发,其实就是对MotionEvent事件的分发过程。即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成:dispatchTouchEvent、onlnterceptTouchEvent和onTouchEvent。
  2. public boolean dispatchTouchEvent(MotionEvent ev)
    用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
    public boolean onInterceptTouchEvent(MotionEvent event)
    在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
    public boolean onTouchEvent(MotionEvent event)
    在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
  3. 当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。

Activity对点击事件的分发过程

  1. 当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,Activity先交由所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
  2. Window是个抽象类,而Window的superDispatchTouchEvent方法也是个抽象方法,Window类可以控制顶级View的外观和行为策略,它的唯一实现位于android.policy.PhoneWindow中,PhoneWindow将事件直接传递给了DecorView。
  3. 我们通过setContentView设置的View是DecorView的一个子View。目前事件传递到了DecorView这里,由于DecorView继承自FrameLayout且是父View,所以最终事件会传递给View。

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

  1. ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null
    。当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget!=null就不成立。那么当ACTION_MOVE和ACTION_UP事件到来时,将导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。
  2. 这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。因此,当面对ACT1ON_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。
  3. 当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理。首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。


    dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法,在上面的代码中child传递的不是null,它会直接调用子元素的dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。如果子元素的dispatchTouchEvent返回true,那么mFirstTouchTarget就会被赋值同时跳出for循环,如果子元素的dispatchTouchEvent返回间false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素的话)。
    [image:07D301F3-9A0D-485F-85A4-62C10CA0D2F1-547-000000AD1C3173DD/E81A3A89-2324-4502-AFCF-7DC645E0F6B1.png]
    其实mFirstTouchTarget真正的赋值过程是在addTouchTarget内部完成的,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果遍历所有的子元素后事件都没有被合适地处理,ViewGroup会自己处理点击事件。


    这里第三个参数child为null,它会调用super.dispatchTouchEvent(event)。很显然,这里就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。

View对点击事件的处理过程

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

    在onTouchEvent中,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用。

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

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

    、


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

View的滑动冲突

滑动冲突的场景

在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。常见的滑动冲突场景可以简单分为如下三种:外部滑动方向和内部滑动方向不一致;外部滑动方向和内部滑动方向一致;上面两种情况的嵌套。

  1. 场景1主要是将ViewPager和Fragment配合使用所组成的页面滑动效果,在这种效果中,可以通过左右滑动来切换页面,而每个页面内部往往又是一个ListView。本来这种情况下是有滑动冲突的,但是ViewPager内部处理了这种滑动冲突,因此采用ViewPager时我们无须关注这个问题,如果我们采用的不是ViewPager而是ScrollView等,那就必须手动处理滑动冲突了。除了这种典型情况外,还存在其他情况,比如外部上下滑动、内部左右滑动等。
  2. 场景2稍微复杂一些,这种场景主要是指内外两层同时能上下滑动或者内外两层同时能左右滑动。
  3. 场景3是场景1和场景2两种情况的嵌套,虽然说场景3的滑动冲突看起来更复杂,但是它是几个单一的滑动伸突的叠加,因此只需要分别处理内层和中层、中层和外层之间的滑动冲突即可,而具体的处理方法其实是和场景1、场景2相同的。

滑动冲突的处理规则

  1. 场景1的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部View拦截点击事件。可以依据滑动路径和水平方向所形成的夹角,也可以依据水平方向和竖直方向上的距离差来判断是水平滑动还是竖直滑动。
  2. 对于场景2,一般都能在业务上找到突破点,比如业务上有规定:当处于某种状态时需要外部View响应用户的滑动,而处于另外—种状态时则需要内部View来响应View的滑动,根据这种业务上的需求我们也能得出相应的处理规则。
  3. 对于场景3,同样还是只能从业务上找到突破点。

滑动冲突的解决方法

抛开滑动规则不说,我们需要找到一种不依赖具体的滑动规则的通用的解决方法。

  1. 外部拦截法:所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下所示。


    上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改。在onlnterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;最后是ACTION_UP事件,这里必须要返回false。
    假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onlnterceptTouchEvent方法在ACTION_UP时返回了false。
  2. 内部拦截法:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法。


    当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值