ViewGroup事件分发源码—ACTION_POINTER_DOWN事件的传递(二)
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,再拉入微信群哦~
本文地址:https://blog.csdn.net/jsonChumpKlutz/article/details/107590616
推荐阅读
-
ViewGroup事件分发源码—ACTION_POINTER_DOWN事件的传递(二)
-
彻底弄清事件分发流程之ViewGroup源码详细分析
-
Android开发知识(九):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(下)
-
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(上)
-
Android开发知识(八):Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)
-
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(下)
-
从点击屏幕到事件处理的事件分发源码流程
-
ViewGroup事件分发源码—ACTION_POINTER_DOWN事件的传递(二)
-
Android事件处理机制:事件分发、传递、拦截、处理机制的原理分析(中)
-
彻底弄清事件分发流程之ViewGroup源码详细分析