View事件分发源码—ACTION_POINTER_DOWN事件的传递
Android版本: 基于API源码28,Android版本9.0。
一 写在前面
在读本篇之前,需要先了解ViewGroup#dispatchTouchEvent()方法源码分析和Android中的多点触控机制。
二 本篇主题
多点触控,想必对于绝大多数Android开发者来说并不陌生,日常开发中或多或少的都会遇到过,比如图片预览中的双指缩放,当然这只是一种简单的场景。
实践出真理,最近在处理多点触控事件中发现 :当有第二根手指触碰屏幕时,ViewGroup接收到了ACTION_POINTER_DOWN事件然后把事件传递给子View,子View接收到的事件并不完全是ACTION_POINTER_DOWN事件,也有可能是ACTION_DOWN事件。
好吧,这确实跟我之前所了解的多点触控方面的知识有所不同。本篇的主题就是在多点触控场景下(比如自定义手柄、键盘),从源码分析ACTION_POINTER_DOWN事件何时会转换成ACTION_DOWN事件。这里只分析ViewGroup#dispatchTransformedTouchEvent()方法的逻辑。
三 源码分析
当ViewGroup开始分发事件给子View或者是自身的时候,会调用其dispatchTransformedTouchEvent()方法:
//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//cancel事件的分发,这里不是重点。
//省略源码。。。 以下都是重点。
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
//省略源码。。。
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 执行任何必要的转换跟分发。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//省略源码。。。
handled = child.dispatchTouchEvent(transformedEvent);
}
//省略源码。。。
return handled;
}
首先,将源码精简到能够分析具体问题的程度。在多点触控场景下,Android系统接收到一个Touch事件的时候,会将Touch事件的发起者——手指或者触控笔之类的,抽象成一个事件指针(Pointer),每个Pointer都会有相对应的Id,该Id再该Pointer产生的系列事件中是唯一的,也必须是唯一的,这样才能在多点触控的场景中准确的追踪某根手指所产生的所有事件。下面会以Pointer来表示事件指针。
下面开始分析方法源码,cancel事件的分发这里先不谈论。event.getPointerIdBits()方法获取的是一个Int类型(4个字节32位,最高位为符号位),Android系统源码中为了节约内存,经常用一个Int类型的数值去保存一些标记位,毕竟Int除去符号位之外还有31位,每一位都能保存一个真值(真值其实就是该位为1),每个真值都可标记一种状态。
方法源码中采用oldPointerIdBits局部变量去保存当前屏幕所有Pointer的pointer id的位掩码,每个新产生的pointer id,都会在oldPointerIdBits变量的第id + 1位上标记为真,比如:pointer id为0,那么oldPointerIdBits变量的第1位就是1,依次类推。Pointer的Id是从0开始,每个新的Pointer都会依次加一。如果用一个Int类型来顺序的记录屏幕中所有的Pointer时,0这个Id``值其实是很尴尬,从计数的角度讲,Id为0的Pointer是第一个Pointer,为了正确、顺序的记录所有的Pointer,系统采用1 << pointer id的操作从另外一个角度将pointer id转换成一个类似于位掩码的值。以下经过这样的操作获取到的值,称为pointer id的位掩码。
getPointerIdBits()方法调用的是MotionEvent类的,具体源码分析如下:
//MotionEvent.java
public final int getPointerIdBits() {
int idBits = 0;
final int pointerCount = nativeGetPointerCount(mNativePtr);
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << nativeGetPointerId(mNativePtr, i);
}
return idBits;
}
nativeGetPointerCount()是native方法,获取屏幕中所有Pointer的数量。在处理点击事件时常用的方法event.getPointerCount()也是调用的同一个native方法。获取到Pointer的数量之后,开始遍历调用nativeGetPointerId()方法来获取pointer id,pointer id是Int类型,且取值是从0开始递增依次为0、1、2、3…(具体细节,可以查看MotionEvent#getPointerId()方法的注释)。其实,pointer id的获取一般是通过pointer index去获取的,也就是event.getActionIndex()的返回值,当然如果目的仅仅是为了获取所有Pointer的Id,也是可以通过上述的方法遍历获取。
获取到pointer id之后,还要进行一系列的位操作。下面举例说明那些二进制操作符的实际意义:
操作:1 << nativeGetPointerId(mNativePtr, i);
当屏幕中只有一个触摸点的时候,nativeGetPointerId()的返回值为0,当屏幕中第二个触摸点按下的时候,nativeGetPointerId()的返回值为1。当有多个触摸点的时候其值从0开始依次递增1。一般开发中,最多也就处理4个Pointer,再多的话怕是产品疯了,所以下面最多就拿4个Pointer举例。:
//当第一根手指按下:
nativeGetPointerId() = 0 -> 然后 1 左移 0 位 = 十进制是 1 二进制 0001;
//第二根手指按下:
nativeGetPointerId() = 1 -> 然后 1 左移 1 位 = 十进制是 2 二进制 0010;
//第三根手指按下:
nativeGetPointerId() = 2 -> 然后 1 左移 2 位 = 十进制是 4 二进制 0100;
//第四根手指按下:
nativeGetPointerId() = 3 -> 然后 1 左移 3 位 = 十进制是 8 二进制 1000;
....
是不是发现了什么规律,每个pointer id经过左移操作的之后,在第pointer id+1位上都是1。pointer id为4的话,那么结果的第5位上就是1,这里1 << nativeGetPointerId()操作结果值就是本篇中pointer id的位掩码。接着分析下一个|=操作符:
//MotionEvent.java
for (int i = 0; i < pointerCount; i++) {
idBits |= 1 << nativeGetPointerId(mNativePtr, i);
}
idBits是一个Int类型的值,每次循环都会将上述操作之后的值进行 同位或 运算,结合上面的例子大致的运算过程如下:
idBits 初始二进制值 0000:
0000 |= 0001 -> 0001
0001 |= 0010 -> 0011
0011 |= 0100 -> 0111
0111 |= 1000 -> 1111
...
最后idBits的十进制值是15,二进制是1111。
用一个Int类型的值,把系统中所有pointer id记录下来,这是系统源码中典型的节约内存的操作。为什么要记录所有的pointer id呢?Touch事件是由硬件产生,并通过特定的机制传输到应用层,Android应用层会将事件包装成MotionEvent,并确定pointer id(硬件是可以标记触摸点的),之后将MotionEvent类下发给所有的View。Pointer是抽象的概念,当View接收到Touch事件的时候,并不知道该事件是由那个Pointer产生的,Pointer相关的数据都是通过native方法获取的,所以当View收到某个Touch事件时需要先获取当前事件的pointer id,然后跟屏幕中已存在的pointer id相比(这些pointer id也可以用数组来表示,为了节约内存系统采用的32位的Int类型来存储)想比较,结果如果相等了,大概率(ViewGroup在处理多点触控机制的时候,可能手动的会合多个pointer id)说明当前事件不是新的Pointer发起的,不是一个新的Pointer那么事件就不会经过特殊的转换直接就可以分发给View。
结合源码,oldPointerIdBits局部变量接收了event.getPointerIdBits()方法的返回值,也就是上面分析的idBits的值。oldPointerIdBits保存了屏幕中所有的pointer id。
接着源码分析:
//ViewGroup#dispatchTransformedTouchEvent()方法
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
if (newPointerIdBits == oldPointerIdBits) {
//分发事件。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
} else {
//不是的话,就去解析事件。
transformedEvent = event.split(newPointerIdBits);
}
先了解下变量的意义,desiredPointerIdBits的取值是由当前事件的pointer id,进行特定的位运算之后的结果,就跟event.getPointerIdBits()里面的操作一样。其源码在ViewGroup#dispatchTouchEvent()方法中:
//ViewGroup#dispatchTouchEvent()方法
final int actionIndex = ev.getActionIndex(); // always 0 for down
//split 一般都是true,所以源码就可以精简。
final int idBitsToAssign = 1 << ev.getPointerId(actionIndex);
//desiredPointerIdBits大部分情况下就是idBitsToAssign
desiredPointerIdBits表示的二进制值就是这样的:0001、0010、0100、1000。举例就是,当第一根手指接触屏幕的时候,desiredPointerIdBits的值就是0001。第一根手指未抬起时,第二根手指接触屏幕时,desiredPointerIdBits的值就是0010。
oldPointerIdBits表示的二进制值上面已经解释过了。举例就是,第一根手指接触屏幕的时候,oldPointerIdBits的值就是0001。第一根手指未抬起时,第二根手指接触屏幕时,oldPointerIdBits的值就是0011。
oldPointerIdBits跟desiredPointerIdBits做 & 运算——& 运算符就是两个二进制值在相同的位上同为1,结果中相同的位上才是1。比如:
0011 & 0010 = 0010; 0011 & 0100 = 0000;
& 运算之后的结果有三种:0、oldPointerIdBits、desiredPointerIdBits。结合源码就是,newPointerIdBits 的取值有三种:
-
等于0 :
newPointerIdBits = 0的话,说明当前的事件是无指针的事件,没有pointer id,那么就丢弃此次事件。只要当前事件是有指针(Pointer)的,那么oldPointerIdBits值中肯定会包含该指针的Id。 -
等于
oldPointerIdBits:如果
newPointerIdBits == oldPointerIdBits就会执行事件的正常分发逻辑。那什么条件下才能相等呢?先回到newPointerIdBits值的计算公式上:int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;第一种相等的情况就是:在单点触控的场景下,一系列的
DOWN、MOVE、UP事件,只有一个pointer id,那么oldPointerIdBits的值就跟desiredPointerIdBits的值相等,进行&运算 之后的值肯定跟oldPointerIdBits相等。但是为了创造newPointerIdBits == oldPointerIdBits的情况,系统还会将新的pointer id的位掩码,合并到之前的pointer id的位掩码上,这个操作在下一节中会仔细讲。这里的位掩码,就是1 << pointer id的操作。第二种情况就是比较特殊:就是
desiredPointerIdBits的值取的是 -1,负数的二进制需要将原码取反码再补码:-1 的原码是 1 000 0001 (为了演示这边只取8位),最高位是符号位,1是负数0是正数。 反码 : 1 111 1110 最高位是符号位不参与计算。 补码 : 1 111 1111 补码就是再末尾补1。所以
-1的二进制就是1 1111,这样的话跟oldPointerIdBits做&操作,其结果就是oldPointerIdBits本身。 -
等于
desiredPointerIdBits:经过上面的例子分析之后,发现只有屏幕中出现了
>= 2个Pointer的时候才会发生,也就是在多点触控场景下。但多点触控也不是必要条件,而是前提条件。
结合源码分析,newPointerIdBits等于0,则选择放弃事件,认为该事件是无指针的事件。如果newPointerIdBits = oldPointerIdBits,那么接下来的if() 语句就会执行,事件就会正常的分发到子View或者ViewGroup自身中。重点来了,当newPointerIdBits不等于oldPointerIdBits的值的时候,会执行:
//ViewGroup#dispatchTransformedTouchEvent()
event.split(newPointerIdBits);
该方法调用的是MotionEvent#split():
//MotionEvent.java
public final MotionEvent split(int idBits) {
MotionEvent ev = obtain();
//省略代码。
int newActionPointerIndex = -1;
int newPointerCount = 0;
for (int i = 0; i < oldPointerCount; i++) {
nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]);
final int idBit = 1 << pp[newPointerCount].id;
if ((idBit & idBits) != 0) {
if (i == oldActionPointerIndex) {
newActionPointerIndex = newPointerCount;
}
map[newPointerCount] = i;
newPointerCount += 1;
}
}
//省略代码。
final int newAction;
if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) {
//省略代码。
if (newPointerCount == 1) {
newAction = oldActionMasked == ACTION_POINTER_DOWN
? ACTION_DOWN : ACTION_UP;
}
//省略代码。
} else {
// Simple up/down/cancel/move or other motion action.
newAction = oldAction;
}
//省略代码。
return ev;
}
}
改代码片段大多数调用的都是本地的方法,方法没有注释其返回值的含义很难懂,所以这边只针对具体问题分析具体的源码。首先,该方法中能看到一处代码,精简之后如下:
//MotionEvent.java
if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) {
if (newPointerCount == 1) {
newAction = oldActionMasked == ACTION_POINTER_DOWN ? ACTION_DOWN : ACTION_UP;
}
}
该转换只是针对ACTION_POINTER_DOWN和ACTION_POINTER_UP事件,如果newPointerCount == 1的话就会执行if 语句:ACTION_POINTER_DOWN会转变成ACTION_DOWN,ACTION_POINTER_UP会转换成ACTION_UP事件 。暂且不说什么情况下newPointerCount == 1,本篇的答案就在这里。当事件转换结束之后,新的事件会继续分发下去,所以View可能在第二根手指按下的时候,接收到的是ACTION_DOWN事件,而不是ACTION_POINTER_DOWN事件。
MotionEvent#split()方法的源码确实看不懂,所以这边就不分析newPointerCount == 1的情况。
四 结论
当ViewGroup接收到某个Pointer产生的ACTION_POINTER_DOWN事件的时候,如果pointer id的位掩码跟MotionEvent获取的所有的pointer id的位掩码都不相同,那么就会调用MotionEvent#split()方法,该方法中,会根据具体的算法决定ACTION_POINTER_DOWN事件是否被转换成ACTION_DOWN事件。split()方法源码里面有太多的native方法,实在没办法深入分析了。。。
那么具体在什么场景下MotionEvent#split()方法才会被执行呢?由于本篇的篇幅已经很长了,所以这里先列举一个方法执行的场景,下篇再从源码的角度详细分析所有的场景:
//某ViewGroup下有两个子View。
findViewById(R.id.first).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
findViewById(R.id.two).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});

如图,当第一根手指触摸View Two的时候,View Two消费了ACTION_DOWN事件。然后第一根手指未抬起,第二根手指触摸View One,这时父ViewGroup中接收到的事件是ACTION_POINTER_DOWN事件,但是传到View One中的事件却是ACTION_DOWN事件。
日志如下:
E/WANG: 父ViewGroup#dispatchTouchEvent():event = 0
E/WANG: View Two#dispatchTouchEvent():event = 0
E/WANG: 父ViewGroup#dispatchTouchEvent():event = 261
E/WANG: View One #dispatchTouchEvent():event = 0
E/WANG: 父ViewGroup#dispatchTouchEvent():event = 6
E/WANG: View Two#dispatchTouchEvent():event = 1
E/WANG: 父ViewGroup#dispatchTouchEvent():event = 1
E/WANG: View One #dispatchTouchEvent():event = 1
结尾:
有兴趣的话可以先加入qq交流群684891631,再拉入微信群哦~
ACTION_DOWN与ACTION_POINTER_DOWN
6113





