美文网首页Androidandroid进阶android技术
Android 点击和滑动事件分发拦截消费流程的源码解读

Android 点击和滑动事件分发拦截消费流程的源码解读

作者: liyihuanx | 来源:发表于2021-10-25 18:00 被阅读0次

    为了写这篇文章,我反复的看了好几十遍源码。而且写的时候时间间隔比较长,有时候写着写着自己都混乱了,又去看一遍源码去分析,所以可能会重复的内容比较多也会稍微乱一点,不过我相信你跟着源码和这边文章一步一步走,应该还是会有收获的!

    本片文章将会介绍,view事件是怎么传递的和分发的,以及点击滑动冲突产生的原因和解决办法。这些都会通过阅读源码解决~

    一些基础的知识

    MotionEvent

    当手指接触屏幕时,会先触发ActionDown一次,然后会触发一次或多次ActionMove,最后触发一次ActionUp


    image.png

    事件分发,拦截,消费

    这是三个方法分别在activity,viewGroup,View中的存在状况
    本片文章也是一直围绕这三个方法做不同情况的解读

    image.png

    简陋事件分发图

    这里堆叠的是一个个view,因为view都是一个个堆叠在屏幕上的


    image.png

    onTouch 和 onClick

    首先看一段简单的代码,给button设置onTouch和onClick事件。我们可以知道,
    当onTouch事件 return false时,onClick 会执行
    当onTouch事件 return true时,onClick 不会执行
    结果我们都知道,也知道是onTouch拦截了才不会执行onClick。
    但是这两段简单的代码在源码中是怎么体现的呢?


    image.png

    因为Button属于一个View,所有我们直接进入view的源码,
    看他的dispatchTouchEvent事件分发的方法,
    可以知道第一个if判断的结果,是会影响第二个if语句的执行的


    image.png

    首先在第一个if看到了我们的onTouch事件

    if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    

    我们把if条件里的判断拆解一下
    1.li != null && li.mOnTouchListener != null
    首先可以看到 li = mLisenterInfo,mLisenterInfo是什么呢?
    我们回设置onTouch事件调用的setOnTouchListener方法,进入查看,可以看到getListenerInfo()

    image.png

    接着再进入getListenerInfo()查看

    image.png
    通过这两幅图,我们可以知道的是
    li = mLisenterInfo != null,
    li.mOnTouchListener !=null(在setOnTouchListener赋值了我们传入的值)
    所以第一个条件是成立的

    2.(mViewFlags & ENABLED_MASK) == ENABLED
    这个条件不用做过多的解读,就是判断能不能点击
    所以第二个条件是成立的

    3.li.mOnTouchListener.onTouch(this, event)
    这调用的方法就是我们给button设置的onTouch了

    image.png
    li.mOnTouchListener.onTouch(this, event) return false ---> result = false
                                             return true ---> result = true 
    
    
    // 第二个if语句
    if (!result && onTouchEvent(event)) { }
    

    3.1 假设我们 return false,那么第一个if语句就失效了不能进入,result还是初始值fasle
    所以我们会执行第二个if ---> 执行onTouchEvent(event),事件消费

    image.png

    接着进入performClick,最终可以看到我们的onClick方法的调用


    image.png

    3.2 假设我们 return true,那么第一个if语句就失效了能进入,result被赋值为true
    所以我们不会执行第二个if ---> 也就不能执行事件消费onTouchEvent(event),也就不能执行到onClick方法了

    再从事件分发流程的角度来看

    首先进入viewGroup#dispatchTouchEvent,分析一个正常的Down事件

    image.png

    注意:if(!canceled && !intercepted){} 这一个if语句里面的代码块,全都是与事件分发相关的,可以说只要进入了一个if语句,就会执行事件分发,接下来也是对if语句里面的代码进行分析

    image.png

    题外话
    这里我们先来看2注释中buildTouchDispatchChildList方法 它最后会执行到buildOrderedChildList
    里面将所有childView按照Z轴的大小,从小到大排序,
    最小的在最前面,最大的在后面

    image.png

    Z轴是怎么来的呢,我们知道一个layout布局里面的所有View都是一个个叠加上去的就像这样,所以最底层的Z轴越小,越排在列表的前面。所以遍历的时候,也是从最后一个拿的


    image.png

    再看一下4注释中isTransformedTouchPointInView方法
    比如你点击的是图中的小圆圈,当他遍历button1时,就会根据你点击的坐标和button1的区域做比较,看看是不是在自己的范围内,不是的话continue继续遍历下一个childview

    image.png

    再看一下4注释下面的方法newTouchTarget = getTouchTarget(child);

    image.png

    题外话结束

    继续往下走的话,会进入到dispatchTransformedTouchEvent

    image.png

    因为child != null 进入else语句,接着就会调用child.dispatchTouchEvent(transformedEvent)
    在上述案例中,button就是child,所以事件就这样分发给了button去做后续操作
    button中没有重写dispatchTouchEvent,所以就进入View的dispatchTouchEvent
    也就是回到我们一开始分析的结果了

    image.png

    可以看到,如果button处理或者消费事件或者在onTouch返回true(也算是处理),就会返回true。进而child.dispatchTouchEvent(transformedEvent)的结果就是true

    image.png

    接着回到之前的方法,if语句会被命中,接着会进入
    而if语句最后会break掉,就直接退出了本次for循环了,本次事件就被button处理了,也不会被其他或者父view获取了,接着就会开始下一个view或者viewgroup了的时间分发了。


    image.png image.png

    另外这里有两个红框的语句,会得到三个条件,特别注意一下
    newTouchTarget = mFirstTouchTarget != null
    mFirstTouchTarget.next = null
    alreadyDispatchedToNewTouchTarget = true

    这里把之前的代码折叠起来了,为了方便看。
    最后的语句因为上面的上面的条件而不会命中,
    最后整个if (!canceled && !intercepted) 里的代码块就结束了

    image.png

    接着往下走,因为mFirstTouchTarget != null 所以我们来看else语句的代码块


    image.png image.png

    最后将handled的结果return,dispatchTouchEvent方法结束


    image.png

    这里Down事件结束

    滑动冲突

    上面只是正常的down事件分发
    接下来用这个例子来看一下有冲突的事件分发来分析一下down和move事件

    先介绍一下情况,布局是这样的:
    自定义了一个BadViewpager,里面放着一个listview,listview里面有很多item,超过一屏幕
    所以这里viewpager是父view,listview就是子view
    BadViewpager正常情况下是可以左右滑动的
    listview正常情况下是可以上下滑动的


    image.png

    BadViewpager,重写了onInterceptTouchEvent拦截事件的方法,并且返回true(为了制造冲突)

    image.png

    如果 BadViewpager的onInterceptTouchEvent返回ture,拦截事件

    此时viewpager是可以左右滑动的,但是listview不能上下滑动

    也就是说事件分发到viewpager就被拦截了,让我们来看看viewpager是怎么拦截事件,并且自己消费事件的。
    我们从头开始,回到ViewGroup的dispatchTouchEvent

    注意:现在是ACTION_DOWN事件
    因为在viewpager重写了onInterceptTouchEvent方法,导致intercepted的为true

    image.png

    我们知道if (!canceled && !intercepted) {}是将事件分发给子View的关键代码块
    intercepted是true 就表示if语句不能进入,就不能将事件分发给子View(也就是listview)

    image.png

    而且mFirstTouchTarget是在if (!canceled && !intercepted) {}里面赋值的,所以往下走的话
    注意这里传入的child是null,因为没有取消事件所以canceled为false

    image.png

    所以进入dispatchTransformedTouchEvent后我们可以看到
    这里直接调用了自己的dispatchTouchEvent,就把事件分发给自己的

    image.png
    这里ACTION_DOWN事件就结束了

    如果 BadViewpager的onInterceptTouchEvent返回false,不拦截事件

    此时viewpager是不可以左右滑动的,但是listview能上下滑动

    ACTION_DOWN事件流程和上面button的事件分发情况是一样的,就不分析了。
    所以这里是分发了一次ACTION_DOWN事件后,
    再次执行dispatchTouchEvent分发ACTION_MOVE。
    所以有几个条件要注意
    newTouchTarget = mFirstTouchTarget != null
    mFirstTouchTarget.next = null
    alreadyDispatchedToNewTouchTarget = false(注意这里有不同)
    因为每次执行dispatchTouchEvent,
    alreadyDispatchedToNewTouchTarget 都会被重置
    而alreadyDispatchedToNewTouchTarget 字段只有在分发子view时才会被赋值为true
    但是根据下图的判断,在move事件中是不会执行下面的语句的

    image.png

    这里ACTION_MOVE事件

    image.png

    所以我们接分析下面的else语句


    image.png

    进入dispatchTransformedTouchEvent我们可以看到


    image.png

    这里ACTION_MOVE事件结束

    内部拦截和外部拦截

    先来看一下内部拦截

    注意内部拦截是子view和父view都要进行处理的
    在子view(listview)中


    image.png

    在父view(viewpager)中


    image.png

    首先来看一下getParent().requestDisallowInterceptTouchEvent()
    传入true时 mGroupFlags | FLAG_DISALLOW_INTERCEPT
    传入false时 mGroupFlags & FLAG_DISALLOW_INTERCEPT

    image.png

    所以到dispatchTouchEvent时(mGroupFlags & FLAG_DISALLOW_INTERCEPT)
    传入true时,运算结果就是 !=0,disallowIntercept为true,intercepted为false,后续事件会正常分发
    传入false时,运算结果就是 =0,disallowIntercept为false,intercepted根据onInterceptTouchEvent情况定

    image.png

    接下来再分析一下,为什么要再父view做处理,而不是直接返回true就行了
    最简单的:不做处理返回true 子view就根本不会接收到事件。内部拦截的代码都不会执行。我认为这只是其中一个原因

    根据这个案例来说,viewgroup分发事件给listview,listview也是一个ViewGroup。
    所以它也会走ViewGroup的dispatchTouchEvent,这个时候问题就来了。
    在分发ACTION_DOWN,会执行一个重置方法


    image.png

    这边将mGroupFlags 做了运算


    image.png

    再回到dispatchTouchEvent方法中时,mGroup又做了运算,
    所以最终的值就是这样一个操作
    mGroupFlags & ~FLAG_DISALLOW_INTERCEPT &FLAG_DISALLOW_INTERCEPT ,导致这个值肯定为0

    所以disallowIntercept为false,直接进入下面的if语句
    此时如果没在父view对onInterceptTouchEvent的down事件做处理的话,返回true
    那么intercepted就会为true

    image.png image.png

    cancel事件的产生

    还是继续用这个案例,还是用内部拦截去分析,当down事件结束后,我们会进入到move事件
    首先move事件进来时,是listview拿着这个事件,他会执行他自己的这个方法

    image.png

    由于上面知道传入false会导致disallowIntercept为false,intercepted根据onInterceptTouchEvent情况定

    image.png

    而此时的ViewGroup返回是true,所以intercepted为true,就会导致如下的执行


    image.png

    执行dispatchTransformedTouchEvent 并且下面会对mFirstTouchTarget赋值
    前面已经分析过了,next就是null,所以这里是将mFirstTouchTarget赋值为null

    image.png

    进入dispatchTransformedTouchEvent查看一下,
    我们可以知道,这里是取消掉子view的事件的


    image.png

    这个move事件执行完后,接着还是move事件,因为他是触发多次的,
    这里就会直接走mFirstTouchTarget == null 的判定了,这时就是父view拿到事件了
    所以就可以从listview的上下滑动转换到viewpager的左右滑动


    image.png

    结尾

    外部拦截的流程就不分析了,分析下来其实和cancel事件产生的流程大同小异,就不做重复了。

    相关文章

      网友评论

        本文标题:Android 点击和滑动事件分发拦截消费流程的源码解读

        本文链接:https://www.haomeiwen.com/subject/ysocaltx.html