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

微信扫一扫,分享到朋友圈

ViewGroup事件分发源码—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 局部变量去保存当前屏幕所有 Pointerpointer id 的位掩码,每个新产生的 pointer id ,都会在 oldPointerIdBits 变量的第 id + 1 位上标记为真,比如: pointer id0 ,那么 oldPointerIdBits 变量的第 1 位就是 1 ,依次类推。 PointerId 是从 0 开始,每个新的 Pointer 都会依次加一。如果用一个 Int 类型来顺序的记录屏幕中所有的 Pointer 时, 0 这个 Id``值其实是很尴尬,从计数的角度讲,Id0Pointer 是第一个 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 idpointer idInt 类型,且取值是从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 类下发给所有的 ViewPointer 是抽象的概念,当 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

oldPointerIdBitsdesiredPointerIdBits& 运算—— & 运算符就是两个二进制值在相同的位上同为1,结果中相同的位上才是1。比如:

0011 & 0010 = 0010; 0011 & 0100 = 0000;
复制代码

& 运算之后的结果有三种:0、 oldPointerIdBitsdesiredPointerIdBits 。结合源码就是, newPointerIdBits 的取值有三种:

  1. 等于0 :

    newPointerIdBits = 0 的话,说明当前的事件是无指针的事件,没有 pointer id ,那么就丢弃此次事件。只要当前事件是有指针( Pointer )的,那么 oldPointerIdBits 值中肯定会包含该指针的 Id

  2. 等于oldPointerIdBits :

    如果 newPointerIdBits == oldPointerIdBits 就会执行事件的正常分发逻辑。那什么条件下才能相等呢?先回到 newPointerIdBits 值的计算公式上:

    int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
    复制代码

    第一种相等的情况就是:在单点触控的场景下,一系列的 DOWNMOVEUP 事件,只有一个 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 本身。

  3. 等于desiredPointerIdBits:

    经过上面的例子分析之后,发现只有屏幕中出现了 >= 2Pointer 的时候才会发生,也就是在多点触控场景下。但多点触控也不是必要条件,而是前提条件。

结合源码分析, 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_DOWNACTION_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
复制代码

微信扫一扫,分享到朋友圈

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

java基础(二)--main方法讲解

上一篇

Linux网络设备驱动之网络设备的打开与释放(四)

下一篇

你也可能喜欢

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

长按储存图像,分享给朋友