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,再拉入微信群哦~
我的Github
我的掘金
我的简书
我的CSDN