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

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
我的掘金
我的简书

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值