美文网首页
ViewGroup对点击事件的分发机制

ViewGroup对点击事件的分发机制

作者: wuchao226 | 来源:发表于2020-08-18 15:13 被阅读0次

    Activity 对点击事件的分发机制
    View 对点击事件的分发机制

    1. 源码分析

    从上面Activity事件分发机制可知,ViewGroup 事件分发机制从 dispatchTouchEvent() 开始。
    注:a. Android 5.0后,ViewGroup.dispatchTouchEvent()的源码发生了变化(更加复杂),但原理相同;

    // Android 5.0前的版本
    /**
     * 源码分析:ViewGroup.dispatchTouchEvent()
     */ 
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ... 
        // 重点分析1:ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
        // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
        // 判断值2:!onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
        // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断
        // c. 关于onInterceptTouchEvent() ->>分析1
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
           ev.setAction(MotionEvent.ACTION_DOWN);  
           final int scrolledXInt = (int) scrolledXFloat;  
           final int scrolledYInt = (int) scrolledYFloat;  
           final View[] children = mChildren;  
           final int count = mChildrenCount;  
           
           // 重点分析2
           // 通过for循环,遍历了当前ViewGroup下的所有子View
           for (int i = count - 1; i >= 0; i--) {  
               final View child = children[i]; 
               if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                            || child.getAnimation() != null) {  
                  child.getHitRect(frame);  
                  // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
                  // 若是,则进入条件判断内部
                  if (frame.contains(scrolledXInt, scrolledYInt)) { 
                      final float xc = scrolledXFloat - child.mLeft;  
                      final float yc = scrolledYFloat - child.mTop;  
                      ev.setLocation(xc, yc);  
                      child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 
    
                      // 条件判断的内部调用了该View的dispatchTouchEvent()
                      // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
                      if (child.dispatchTouchEvent(ev))  { 
                          mMotionTarget = child;  
                          // 调用子View的dispatchTouchEvent后是有返回值的
                          // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                          // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                          // 即把ViewGroup的点击事件拦截掉
                          return true; 
                      }
                  }
               }
           }
        }
      
         boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
                        (action == MotionEvent.ACTION_CANCEL);  
         if (isUpOrCancel) {  
             mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
         }  
         final View target = mMotionTarget;
         // 重点分析3
         // 若点击的是空白处(即无任何View接收事件) / 拦截事件(手动复写onInterceptTouchEvent(),从而让其返回true)
         if (target == null) {  
             ev.setLocation(xf, yf); 
             if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                 ev.setAction(MotionEvent.ACTION_CANCEL);  
                 mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; 
             }
             // 调用ViewGroup父类的dispatchTouchEvent(),即View.dispatchTouchEvent()
             // 因此会执行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即自己处理该事件,事件不会往下传递(具体请参考View事件的分发机制中的View.dispatchTouchEvent())
             // 此处需与上面区别:子View的dispatchTouchEvent()
             return super.dispatchTouchEvent(ev);
         }
         ...
    }
    
    
    /**
      * 分析1:ViewGroup.onInterceptTouchEvent()
      * 作用:是否拦截事件
      * 说明:
      *     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
      *     b. 返回false = 不拦截(默认)
      */
      public boolean onInterceptTouchEvent(MotionEvent ev) {  
    
        return false;
    
      } 
      // 回到调用原处
    

    2. 流程解析

    a. 结论:Android事件分发总是先传递到ViewGroup、再传递到View
    b. 过程:当点击了某个控件时

    3. 方法总结

    核心方法主要是:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

    3.1 dispatchTouchEvent()

    a. 调用时刻:事件从Activity传递过来时
    b. 返回的结果有3种:


    3.2 onInterceptTouchEvent()

    a. 调用时刻:在ViewGroup的dispatchTouchEvent() 内部调用
    b. 返回的结果有2种:


    3.3 onTouchEvent()

    a. 调用时刻:ViewGroup父类的dispatchTouchEvent(),即super.dispatchTouchEvent()时
    b. 返回的结果有2种:


    基于 Android-28 的源码上进行分析 touch 事件分发时序

    ViewGroup

    ViewGroup 是一组 View 的组合,在其内部有可能包含多个子 View,当手指触摸屏幕上时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。

    因此它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:

    1. 当前 Group 是否需要拦截 touch 事件;

    2. 是否需要将 touch 事件继续分发给子 View;

    3. 如何将 touch 事件分发给子 View。

    View

    View 是一个单纯的控件,不能再被细分,内部也并不会存在子 View,所以它的事件分发的重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等)。

    1. 是否存在 TouchListener;

    2. 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。

    事件分发核心 dispatchTouchEvent

    整个 View 之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是 dispatchTouchEvent 方法。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。

    先从宏观角度,纵览整个 dispatch 的源码如下:


    如代码中的注释,dispatch 主要分为 3 大步骤:

    • 步骤 1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再会传递给子 View(或者以 CANCEL 的方式通知子 View)。

    • 步骤 2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将 mFirstTouchTarget 赋值给捕获 touch 事件的 View。

    • 步骤 3:根据 mFirstTouchTarget 重新分发事件。

    接下来详细的看下每一个步骤:

    步骤 1 的具体代码如下

    图中红框标出了是否需要拦截的条件:

    • 如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 进行拦截判断;

    • 或者 mFirstTouchTarget 不为 null,代表已经有子 View 捕获了这个事件,子 View 的 dispatchTouchEvent 返回 true 就是代表捕获 touch 事件。

    如果在上面步骤 1 中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤 2。

    步骤 2 具体代码如下

    仔细看上述的代码可以看出:

    • 图中 ① 处表明事件主动分发的前提是事件为 DOWN 事件;

    • 图中 ② 处遍历所有子 View;

    • 图中 ③ 处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;

    • 图中 ④ 处调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,如果子 View 捕获事件成功,则将 mFirstTouchTarget 赋值给子 View。

    步骤 3 具体代码如下

    步骤 3 有 2 个分支判断。

    • 分支 1:如果此时 mFirstTouchTarget 为 null,说明在上述的事件分发中并没有子 View 对事件进行了捕获操作。这种情况下,直接调用 dispatchTransformedTouchEvent 方法,并传入 child 为 null,最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自身的 onTouchEvent 方法,进行处理 touch 事件。也就是说:如果没有子 View 捕获处理 touch 事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理。

    • 分支 2:mFirstTouchTarget 不为 null,说明在上面步骤 2 中有子 View 对 touch 事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。

    为什么 DOWN 事件特殊

    所有 touch 事件都是从 DOWN 事件开始的,这是 DOWN 事件比较特殊的原因之一。另一个原因是 DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。

    在步骤 2 中,只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的。

    mFirstTouchTarget 有什么作用

    mFirstTouchTarget 的部分源码如下:


    可以看出其实 mFirstTouchTarget 是一个 TouchTarget 类型的链表结构。而这个 TouchTarget 的作用就是用来记录捕获了 DOWN 事件的 View,具体保存在上图中的 child 变量。可是为什么是链表类型的结构呢?因为 Android 设备是支持多指操作的,每一个手指的 DOWN 事件都可以当做一个 TouchTarget 保存起来。在步骤 3 中判断如果 mFirstTouchTarget 不为 null,则再次将事件分发给相应的 TouchTarget。

    CANCEL 事件

    在上面的步骤 3 中,继续向子 View 分发事件的代码中,有一段逻辑:


    上图红框中表明已经有子 View 捕获了 touch 事件,但是蓝色框中的 intercepted boolean 变量又是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild == true。

    看一下 dispatchTransformedTouchEvent 方法的部分源码如下:


    因为之前传入参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。

    什么情况下会触发这段逻辑呢?

    总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。

    实际上有个很经典的例子可以用来演示这种情况:

    当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。

    总结

    分析了 dispatchTouchEvent 的事件的流程机制,这一过程主要分 3 部分:

    • 判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;

    • 在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;

    • 最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。

    然后介绍了整个事件分发中的几个特殊的点。

    • DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;

    • mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;

    • CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。

    相关文章

      网友评论

          本文标题:ViewGroup对点击事件的分发机制

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