欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

ViewGroup事件分发源码—ACTION_POINTER_DOWN事件的传递(二)

程序员文章站 2022-05-09 09:53:37
View事件分发源码(二)—ACTION_POINTER_DOWN事件的传递Android版本: 基于API源码28,Android版本9.0。一 写在前面在读本篇之前,需要先了解:ViewGroup#dispatchTouchEvent()方法源码分析;Android中的多点触控机制; Touch事件分发源码分析—ACTION_POINTER_DOWN事件的传递(一)。二 本篇主题上篇Touch事件分发源码分析—ACTION_POINTER_DOWN事件的传递(一)已经分析了,ACTIO...

View事件分发源码(二)—ACTION_POINTER_DOWN事件的传递

Android版本: 基于API源码28,Android版本9.0。

一 写在前面

在读本篇之前,需要先了解:

ViewGroup#dispatchTouchEvent()方法源码分析

Android中的多点触控机制

Touch事件分发源码分析—ACTION_POINTER_DOWN事件的传递(一)

二 本篇主题

上篇Touch事件分发源码分析—ACTION_POINTER_DOWN事件的传递(一)已经分析了,ACTION_POINTER_DOWN事件转换成ACTION_DOWN事件的过程,但只分析了过程,转换场景分析才是最重要最贴近实际开发的。本篇将从源码的角度去分析,在什么样的场景中才会发生事件的转换。

三 源码分析

通过上篇的源码分析,ViewGroup#dispatchTransformedTouchEvent()方法中的desiredPointerIdBits参数的取值,决定了Touch事件是否需要拆分,如果不需要拆分的话就不会出现ACTION_POINTER_DOWN事件的转换,下面从ViewGroup#dispatchTouchEvent()开始分析,源码精简到只分析具体问题的程度。


场景一 ViewGroup有多个子View需要消费事件,ViewGroup本身不消费事件:

应用场景: 手机自定义虚拟按键,整个虚拟键盘就是一个ViewGroup,所有的虚拟按键都是TextView,且每个按键都需要消费事件,支持多个按键同时按下。典型的多点触控。

先分析ACTION_DOWN事件的分发源码,精简如下:

//ViewGroup#dispatchTouchEvent()方法精简。
                //接收ACTION_DOWN事件。
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    //获取Pointer id的位掩码,也就是将 1 左移 (pointer id) 位的操作。
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
                         //循环找需要消费事件的子View。
                        for (int i = childrenCount - 1; i >= 0; i--) { 
                            //查找符合条件的View是否在事件消费链表中。在的话说明子View中曾有消费了ACTION_DOWN事件的。
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                //合并当前Pointer的pointer id位掩码。
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                     //分发当前事件给子View。       
                   if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {                               //如果子View消费了事件,就将该View添加到消费链表中。
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                }

首先,排除ACTION_HOVER_MOVE事件,只有ACTION_DOWNACTION_POINTER_DOWN事件会被ViewGroup分发到所有的子View中,即使子View不消费事件。ViewGroup接收到ACTION_DOWN事件的时候,先获取事件发起者Pointerpointer id位掩码位掩码 就是:1 << ev.getPointerId(actionIndex)的操作)。事件会按照View绘制的顺序,循环查找符合条件的View,一般事件先分发到View树的最底层。

当找到适合分发事件的View之后,先判断View是否存在于以mFirstTouchTarget为首的事件消费链表中,其结果用newTouchTarget局部变量来表示,默认为nullnewTouchTarget的取值决定了事件分发的走向。ACTION_DOWN事件下,mFirstTouchTarget代表的消费链表为空newTouchTargetnull,循环不会break掉。

接着就会执行dispatchTransformedTouchEvent()方法将ACTION_DOWN事件分发到子View中,此时的参数idBitsToAssign是ACTION_DOWN事件的原始pointer id的位掩码,没有发生合并pointer id的操作

//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
   
        if (newPointerIdBits == oldPointerIdBits) {
           //分发事件给子View
            handled = child.dispatchTouchEvent(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
        return handled;
    }

接收到ACTION_DOWN事件时newPointerIdBits 的值等于oldPointerIdBits的值 。

此时屏幕中就存在一个触摸点,该指针的pointer id0,其位掩码操作为0001。上述源码中event.getPointerIdBits()的取值为0001,等于参数desiredPointerIdBits的值0001。值相等的条件下,if语句成立,事件正常分发给子View。该部分具体源码详解,以及一些计算过程需要详看Touch事件分发源码分析—ACTION_POINTER_DOWN事件的传递(一)

接着,第一根手指未抬起时第二根手指按下。第二根手指按下的时候,可能会按在同一个子View上,也可能会按 在其它的子View上。当大于一个触摸点接触屏幕的时候,系统会产生ACTION_POINTER_DOWN事件,下面看下该事件在ViewGroup中是如何分发的,源码十分精简:

//ViewGroup#dispatchTouchEvent()方法。
                //接收ACTION_POINTER_DOWN事件。
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
          
                         //循环找需要消费事件的子View。
                        for (int i = childrenCount - 1; i >= 0; i--) { 
                            //查找符合条件的View是否在事件消费链表中。
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                //在的话合并pointer id位掩码。
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                //*****注意****:这里结束掉了for循环。
                                break;
                            }
                        }
                }

ViewGroup分发ACTION_POINTER_DOWN事件的流程跟ACTION_DOWN的不同,在于分发过程中newTouchTarget的取值。找到符合分发事件的子View之后,先判断是否存在于以mFirstTouchTarget为首的消费链表中,存在说明newTouchTarget != null,该View之前已经消费了ACTION_DOWN事件,就是第二根手指其实还是按在了同一个子View上。不存在的话newTouchTarget = null,就是第二根手指按在了其它的子View上。

先分析newTouchTarget != null时的场景:

newTouchTarget != nullIf语句成立,此时会将新指针的pointer id的位掩码合并到上一个指针的pointer id位掩码上。这个 |=操作就是其精髓所在。之后结束掉循环 走其它的分发流程,执行分发的源码精简如下:

//ViewGroup#dispatchTouchEvent()方法。
if (mFirstTouchTarget == null) {
   handled = dispatchTransformedTouchEvent(ev, canceled, null,-1);
} else {
   dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits))
 

mFirstTouchTarget不为null会走else逻辑,该逻辑中会调用dispatchTransformedTouchEvent()方法并将target.pointerIdBits作为参数传入,这个target.pointerIdBits就是之前合并过Id位掩码之后的值。只要发生了pointer id位掩码的合并,那么在执行dispatchTransformedTouchEvent()方法时,newPointerIdBits的取值等于oldPointerIdBits的值,事件不会经过转换直接分发到子View中。

以上就是ViewGroup接收到ACTION_POINTER_DOWN事件时newTouchTarget != null的情况。该场景就是:两根手指同时按在一个View上面

newTouchTarget == null场景分析:

newTouchTarget == null就说明当前要消费ACTION_POINTER_DOWN事件的子View不在消费链中,也就是第二根手指按下的View不是之前消费ACTION_DOWN事件的View,这时pointer id的位掩码不会合并,for循环也不会结束掉,走正常的事件分发。

在执行dispatchTransformedTouchEvent()方法的时候,这时ACTION_POINTER_DOWN事件就会被转换成ACTION_DOWN事件,也就是oldPointerIdBits != newPointerIdBits走了else语句,执行event.split()方法。下面分析下为什么这种场景下会发生事件的转换?

ACTION_POINTER_DOWN事件的产生,表示当前的pointer id是属于一个新的Pointer,按照规则,Id会依次增加1,也就是当前事件的pointer id等于 1,位掩码就是0010。而产生ACTION_DOWN事件的PointerId0,位掩码就是0001,那么再执行dispatchTransformedTouchEvent()方法时的大致操作如下:

操作:int oldPointerIdBits = event.getPointerIdBits();
结果:oldPointerIdBits = 0011; 
desiredPointerIdBits = 0010;
操作:int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
结果:newPointerIdBits = 0010;
最后:0011= 0010

newPointerIdBits不等于oldPointerIdBits,执行event.split()方法转换ACTION_POINTER_DOWN事件为ACTION_DOWN事件。该场景就是:ViewGroup有两个子View,第一跟手指按在了一个子View上,第二根手指按在了另外一个子View上。

场景一总结:ViewGroup不拦截事件,子View需要消费事件的情况下。

  • 两根手指同时按在一个子View上时,ACTION_POINTER_DOWN事件会正常的分发到该View中。
  • 第一根手指按在子View (one)上并未抬起时,第二根手指按在了子View(two)上面,这时父ViewGroup中接收到的是ACTION_POINTER_DOWN事件,但是在分发给View(two)时,会将ACTION_POINTER_DOWN事件转换成ACTION_DOWN事件,也就是View(two)中接收到还是ACTION_DOWN事件。这种场景多发生在虚拟按键的组合键上。

场景二 ViewGroup跟子View都需要消费事件:

应用场景: 虚拟按键需要适配某款游戏的时候,父ViewGroup中只处理ACTION_MOVE事件,用于模仿鼠标的移动。子View一般为独立的功能按键,比如刺激战场上的开火、瞄准等按键。父ViewGroup需要消费事件,子View也需要消费事件。

当第一根手指触摸父ViewGroup时,父ViewGroup中接收到ACTION_DOWN事件,父ViewGroup如果不拦截事件的话,会根据触摸点的位置找到合适的子View分发ACTION_DOWN事件,如果子View不消费事件或者是不存在子View,那么事件会交给ViewGroup自身处理。

第一根 手指按下的时候,要么是子View消费了事件,要么是父ViewGroup消费了事件。如果是子View消费了事件,那么mFirstTouchTarget != null,后续事件会继续分发到该View中。如果是父ViewGroup拦截了事件,那么mFirstTouchTarget == null,后续的事件就不会分发到子View中,相当于强制的拦截事件。

针对这两种情况,分析下当第二根 手指按下的时候ACTION_POINTER_DOWN事件的分发过程:

  1. View消费了ACTION_DOWN事件:

    ViewGroup分发ACTION_POINTER_DOWN事件的精简源码:

    //ViewGroup#dispatchTouchEvent()方法。
    //mFirstTouchTarget不等于null。
    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
            intercepted = onInterceptTouchEvent(ev);
       } else {
            intercepted = true;  
       }
    //找子View分发ACTION_POINTER_DOWN事件。
     if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
          final int actionIndex = ev.getActionIndex(); // always 0 for down
          final int idBitsToAssign = 1 << ev.getPointerId(actionIndex);
         //是否找到子View。 
         if (newTouchTarget == null && childrenCount != 0) {
             //找到符合的子View就分发事件。    、
             if (!child.canReceivePointerEvents() 
                 || !isTransformedTouchPointInView(x, y, child, null)) {
                      continue;
                }
          }
         //if语句1:View处理之后执行这里。
          if (newTouchTarget == null && mFirstTouchTarget != null) {
               newTouchTarget.pointerIdBits |= idBitsToAssign;
            }
       }
    //if语句2:此时mFirstTouchTarget不等与null。
    if (mFirstTouchTarget == null) {
       handled = dispatchTransformedTouchEvent(ev, canceled, null,-1);
    } else {
        //会执行这里,target == mFirstTouchTarget的值。
       dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits))
     }
    

    当父ViewGroup接收到ACTION_POINTER_DOWN事件时,mFirstTouchTarget != null,父ViewGroup正常的分发事件,遍历所有的子View,找出符合分发条件的子View,这里只分析没有找到符合分发条件的子View的情况。

    如果没有子View消费ACTION_POINTER_DOWN事件newTouchTarget = null。所以下面的if语句1语句就会执行,然后将新的pointer id的位掩码合并到以存在的Pointer上。只要发生了pointer id的位掩码的合并,ACTION_POINTER_DOWN事件就不会转换成ACTION_DOWN事件。事件将继续分发给之前消费了ACTION_DOWN事件的View

    该种场景下,虽然ACTION_POINTER_DOWNACTION_POINTER_UP事件只会分发到子View中,但是其ViewGroup作为事件的分发者,其内部也会接收到系列事件,所以必要的情况下需要代码跟踪多跟手指的事件,然后手动的将事件分发到所需要的子View中。

    总结一下: 当ViewGroup跟其子View都需要消费事件的时候,如果第一根手指按在了子View上,且并未抬起时,第二根手指按下,如果第二根手指所接触的区域内,没有找到符合分发事件的子View的话,ACTION_POINTER_DOWN事件会继续分发到,消费了ACTION_DOWN事件的子View中。

  2. ViewGroup消费了ACTION_DOWN事件:

    ViewGroup分发ACTION_POINTER_DOWN事件的精简源码:

    //ViewGroup#dispatchTouchEvent()方法。
    final boolean intercepted;
    //mFirstTouchTarget等于null,事件为ACTION_POINTER_DOWN事件。
    if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
             
        } else {
            //会走这里,直接强制拦截事件。
            intercepted = true;
        }
    //这个if不会执行
    if (!canceled && !intercepted) {
    }
    
      //此时mFirstTouchTarget等于null。
    if (mFirstTouchTarget == null) {
       handled = dispatchTransformedTouchEvent(ev, canceled, null,-1);
    } else {
     }
    

    mFirstTouchTarget的赋值是在ViewGroup中有一个子View消费了事件时,所以当ViewGroup本身消费事件的话,mFirstTouchTarget是为null的。那么在接收到ACTION_POINTER_DOWN事件之后,ViewGroup本身会强制的拦截事件,不走事件分发的流程。执行dispatchTransformedTouchEvent()方法时,传入的子Viewnull,也是唯一一处参数值为null的地方,这样事件就会交给自身去处理。

    如果父ViewGroup消费了ACTION_DOWN事件,那么接下来的事件都会强制拦截,不进行事件的分发。所以,在父ViewGroup中需要处理多点触控相关事件,且必要时需要代码跟踪跟踪某个Pointer的事件,然后手动的将事件分发到所需要的子View中。

    总结一下: 当父ViewGroup跟其子View都需要消费事件的时候,如果第一根手指按下的区域内,没有子View消费ACTION_DOWN事件,那么事件将交给父ViewGroup去消费。在第一根手指未抬起时,第二根手指按下,ACTION_POINTER_DOWN事件会继续分发到父ViewGroup中,之后的任何事件都只会分发给父ViewGroup。

四 你能学到的

日常开发中,在处理多点触控场景时,一定要深思熟虑,透彻的分析当前的场景下Touch事件该怎么分发,下面举几个例子:

  1. 比如双指缩放功能,该功能一般作用在一个View上,那么只需要在内部做好Pointer事件的跟踪就好了,对应与场景二中的情景。
  2. 手机中实现一套Window的虚拟键盘,键盘本身是一个自定义的ViewGroup,按键是其中的一个子View,那么在实现按键的组合键的时候,ACTION_POINTER_DOWN事件的分发场景就是场景一
  3. 操控云电脑(概念自己百度一下吧~)界面的场景。腾讯的刺激战场、王者荣耀都有一个操作:一边按着前进的按钮,一边滑动屏幕移动视角。在云电脑中这种操作的实现方式可能是这样的:View层,ViewGroup中有一个SurfaceView来显示画面,还有个同级的FrameLayout来添加虚拟按键的Fragment。事件分发时,父ViewGroup中拦截事件为了操控SurfaceView显示的画面,Fragment中也需要拦截事件来处理虚拟按键。当一只手在操控画面的时候,其父ViewGroup一直在处理事件,这时另一只手点击了虚拟按键,此时的ACTION_POINTER_DOWN事件是不会分发给子View中的,也就是Fragment中压根都接收不到任何的事件,这时就需要父ViewGroup手动的去把ACTION_POINTER_DOWN事件分发给需要事件的View中。该场景对应的是场景二。不过,还是建议不要让父ViewGroup去承担那么多的事件分发,最好能把事件具体到某个View中。比如说,在SurfaceView上在包裹一层ViewGroup来处理操控画面的事件。

结尾:

有兴趣的话可以先加入qq交流群684891631,再拉入微信群哦~

我的Github
我的CSDN
我的掘金
我的简书

本文地址:https://blog.csdn.net/jsonChumpKlutz/article/details/107590616

相关标签: 源码 android