View事件分发源码(二)—ACTION_POINTER_DOWN事件的传递
Android版本: 基于API源码28,Android版本9.0。
一 写在前面
在读本篇之前,需要先了解:
ViewGroup#dispatchTouchEvent()方法源码分析;
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_DOWN和ACTION_POINTER_DOWN事件会被ViewGroup分发到所有的子View中,即使子View不消费事件。 当ViewGroup接收到ACTION_DOWN事件的时候,先获取事件发起者Pointer的pointer id的位掩码(位掩码 就是:1 << ev.getPointerId(actionIndex)的操作)。事件会按照View绘制的顺序,循环查找符合条件的View,一般事件先分发到View树的最底层。
当找到适合分发事件的View之后,先判断View是否存在于以mFirstTouchTarget为首的事件消费链表中,其结果用newTouchTarget局部变量来表示,默认为null。newTouchTarget的取值决定了事件分发的走向。ACTION_DOWN事件下,mFirstTouchTarget代表的消费链表为空newTouchTarget为null,循环不会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 id为0,其位掩码操作为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 != null时If语句成立,此时会将新指针的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事件的Pointer的Id是0,位掩码就是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事件的分发过程:
-
子
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_DOWN、ACTION_POINTER_UP事件只会分发到子View中,但是其ViewGroup作为事件的分发者,其内部也会接收到系列事件,所以必要的情况下需要代码跟踪多跟手指的事件,然后手动的将事件分发到所需要的子View中。总结一下: 当ViewGroup跟其子View都需要消费事件的时候,如果第一根手指按在了子View上,且并未抬起时,第二根手指按下,如果第二根手指所接触的区域内,没有找到符合分发事件的子View的话,ACTION_POINTER_DOWN事件会继续分发到,消费了ACTION_DOWN事件的子View中。
-
父
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()方法时,传入的子View为null,也是唯一一处参数值为null的地方,这样事件就会交给自身去处理。如果父
ViewGroup消费了ACTION_DOWN事件,那么接下来的事件都会强制拦截,不进行事件的分发。所以,在父ViewGroup中需要处理多点触控相关事件,且必要时需要代码跟踪跟踪某个Pointer的事件,然后手动的将事件分发到所需要的子View中。总结一下: 当父ViewGroup跟其子View都需要消费事件的时候,如果第一根手指按下的区域内,没有子View消费ACTION_DOWN事件,那么事件将交给父ViewGroup去消费。在第一根手指未抬起时,第二根手指按下,ACTION_POINTER_DOWN事件会继续分发到父ViewGroup中,之后的任何事件都只会分发给父ViewGroup。
四 你能学到的
日常开发中,在处理多点触控场景时,一定要深思熟虑,透彻的分析当前的场景下Touch事件该怎么分发,下面举几个例子:
- 比如双指缩放功能,该功能一般作用在一个
View上,那么只需要在内部做好Pointer事件的跟踪就好了,对应与场景二中的情景。 - 手机中实现一套
Window的虚拟键盘,键盘本身是一个自定义的ViewGroup,按键是其中的一个子View,那么在实现按键的组合键的时候,ACTION_POINTER_DOWN事件的分发场景就是场景一。 - 操控
云电脑(概念自己百度一下吧~)界面的场景。腾讯的刺激战场、王者荣耀都有一个操作:一边按着前进的按钮,一边滑动屏幕移动视角。在云电脑中这种操作的实现方式可能是这样的: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,再拉入微信群哦~
1266

被折叠的 条评论
为什么被折叠?



