手势事件的产生、传递及处理也许我们并不陌生,特别是在自定义一些可交互性的View
时,处理手势是不可避免的。而日常中我们接触得最多的是单指产生的手势事件,毕竟除了图片缩放、游戏等情况,谁会没事用两根手指或整块手掌在屏幕上滑来滑去。这就导致了关于如何处理多手指下的Touch
事件的相关知识的文章很少,本篇笔记便是对多手指Touch变化处理原则
的一个归纳与记录。
问题
如果我们仔细观察SDK
自带的一些滚动控件的使用效果,便会发现不论是ScrollView
还是ListView
,一根手指滑动或者手指数量不断变化的情况下滑动,它们都能表现正常;
我们再看看市面上的一些流行应用在这方面的处理,举个例子:例如大家所熟悉的美团Android客户端
,其首页的下拉刷新控件
便是一个交互性较强的自定义View
,当我们单指A
向下拖动时,隐藏的“小人儿”逐渐出现,此时我们再放一根手指B
到屏幕上并拖动B
,似乎控件表现正常,但是此时若放开手指A
(保持B
),问题便出现了:当前页面从一个位置瞬间移动到了另一个位置!虽然于业务无关痛痒,但是对于体验,却是不可忽视。既然问题已经抛出,接下来我们就通过对手势的分析来确定它是如何产生的并给出解决方案。
手势基础
在此之前我们先了解手势一些最基本的原则:Touch
手势的事件除了最常接触的ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
及ACTION_CANCEL
外,还包括封装了多指的事件的ACTION_POINTER_DOWN
和ACTION_POINTER_UP
,Android
对手势事件产生的处理原则之一就是:
- 除了第一根手指按下时产生的事件是
ACTION_DOWN
外,保持第一根手指的同时,其余手指按下时产生的事件都是ACTION_POINTER_DOWN
;- 除了最后一根离开屏幕的手指产生的事件是
ACTION_UP
外,期间抬起的其余手指产生的事件都是ACTION_POINTER_UP
。注意:每一个手势事件都包含了当前所有手指的信息,并非仅有当前事件对应的手指信息!
也就是说,一个完整的Touch手势包括了ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
以及在ACTION_DOWN
和ACTION_UP
之间可能产生的ACTION_POINTER_DOWN
、ACTION_POINTER_UP
所有事件。一个好的建议就是处理手势事件时,利用MotionEvent # getActionMasked()
代替MotionEvent # getAction()
来得到完整的事件类型。
ID和Index
同时,为了保证手指的唯一性,系统其实为每一根参与的手指分配了一个唯一标识ID
,在按下抬起期间始终保持不变;此外,根据ID
从小到大排序,每根手指还对应一个索引Index
,索引将随着手指数量变化而变化,但所有索引始终保持从0开始。
当手指按下时,系统则通过如下流程图为手指生成正确的ID
:
- start:手指按下,初始化
- 操作1:定义
a->0, b->0
,分别表示索引和ID - 条件1:
索引a
处是否存在手指 - 条件2:
索引a
处对应的手指的ID
是否等于b
- 操作2:
a+1->a, b+1->b
- 操作3:指定当前按下的手指
ID
为b
,并根据所有手指的ID
从小到大分配对应的索引 - end:封装
ACTION_DOWN
或ACTION_POINTER_DOWN
事件并发送
当抬起手指时,规则就相对简单些:移除对应手指的ID
,并根据剩余的所有手指的ID
从小到大重新分配对应的索引。
如果还不太明白,我们举个简单的例子来分析这个流程:第一步,我们依次按下手指A、B
;第二步,抬起手指A
;第三步,按下手指A、C
;第四步,依次抬起手指A、B、C
。在整个过程中来看看手指ABC
的ID
和索引Index
分别是什么:
第一步:根据按下流程图,
A
的ID == 0,Index == 0
,B
的ID == 1,Index == 1
;第二步:抬起
A
后,在下一个事件到来之前,将移除A
的ID
,只剩B
的ID
,然后重新分配索引,所以B
最终的ID == 1,Index == 0
;第三步:按下手指
A
生成ACTION_POINTERD_DOWN
前发现索引0处对应的ID != 0
,所以将A
的ID
分配为0,然后重新按ID
大小分配索引,最终A
的ID == 0,Index == 0
,B
的ID == 1,Index == 1
;继续按下C
发现索引0处的ID
为0,索引1处的ID
为1,索引2处没有手指,所以将C
的ID
分配为2,最后再次按ID
大小重新分配索引,最终ABC
的[ID,Index]
分别为[0,0], [1,1], [2,2]
;第四步:抬起
A
,剩下BC
的ID
1和2,按ID
大小重新分配索引,BC
的[ID,Index]
分别为[0,1], [1,2]
,抬起B、C
依此类推。
那么ID和Index
到底有什么用呢,平常似乎并没有使用到。我们可以先看看下面这个疑惑:
可能部分朋友在接触多手指事件处理时心中一直有一个疑问:当屏幕上同时有多根手指在移动,Android
是如何决定以哪根手指为准的呢(即:如果是移动视图,当几根手指都在MOVE
时,视图到底是跟随哪一根手指MOVE
的)?准确地说,“视图移动由谁决定”这问题指的是:在MOVE
时,该事件对应的手指到底是哪一根。
其实系统规定 ID最小的手指便是追踪手指。即在MOVE
时,event.getX()/getY()
对应的手指始终是当前按下 的所有手指中ID
最小的。因此,在处理移动事件的时候,要特别注意最小ID
发生变化时的情况,手指数量发生变化时,我们如果可以清楚地推算出各手指的ID和Index
,最小ID
的变化就轻而易举地可以知道。那么接下来我们来尝试分析前面提到的跳动问题的产生原因。
分析原因
既然是移动视图,那么我们就来看看,一个随手指移动的自定义View
在手势事件回调方法里最基本的写法:
1 | private float lastX, lastY; |
上面代码中,定义了lastX, lastY
用来记录 追踪手指 的最近一次坐标,然后在MOVE
时用来和当前最新坐标运算得到手指的移动距离dx, dy
,进行移动等操作。在单指情况下,上面的写法没问题。可以走一遍流程验证:按下手指A,由于是第一根手指,所以A的ID和索引分别为0和0,且事件为ACTION_DOWN
,在ACTION_DOWN
调用getXY()
便得到了A手指的坐标;然后移动手指A,产生ACTION_MOVE
事件且该事件对应的手指的ID和Index
分别为0和0,在此取得该事件产生时手指的坐标,即最新坐标,运算得到移动的距离,进行视图移动,最后更新最近一次坐标lastX'和'lastY
,ACTION_MOVE
再次生成到来时,以此类推。
若此时按下手指B,根据前面的流程图分析,B的ID和Index
分别为1和1,且事件为ACTION_POINTER_DOWN
,继续移动手指(不论A或者B),由于A的ID最小,所以ACTION_MOVE
对应的手指依然为A(说明:不论ACTION_MOVE
对应的手指是谁,只要手指移动了,都会产生相应的事件并封装到了ACTION_MOVE
,只是ev.getXY()
可以直接取到该事件对应手指的坐标),移动依然随着A手指移动。
然后此时我们突然松开手指A,那么在ACTION_POINTER_UP
后,由于A的ID 0被移除,手指的索引被重新分配,最终B的ID和Index
分别为1和0,在继续移动B时,由于此时B的ID最小,所以ACTION_MOVE
对应的手指ID发生了变化,即追踪手指由A变为了B,ev.getXY()
得到的坐标是B当前最新的坐标,而lastXY
缓存的坐标还是手指A在松开前的最终坐标而非手指B的上一个位置的坐标,那么视图就会从上一处位置瞬间移动 A抬起时的位置到B现在位置 这么长的距离,就出现了文章最初的问题。(若松开的是B,由于B的ID并非最小,所以并不影响追踪手指;其实,不论手指有多少,或者如何松开,都是一个道理)
由此便得出了原因:在手指数量变化时,未更新追踪手指的缓存坐标lastX, lastY
。
解决方案
前面提到,由于是追踪手指发生了变化,使得缓存坐标lastX, lastY
记录的依然是上一个追踪手指的最后一次坐标,而导致了瞬间跳动的问题。所以解决方案就是及时更新lastXY,那么,更新lastX, lastY
的时机应该如何挑选,回顾一下,前文提到:
“
lastXY
缓存的坐标还是手指A在松开前的最终坐标而非手指B的上一个位置的坐标”
既然如此,就需要在下一个事件即MOVE
到来前提前修改,手指数量变化有两个时机,先来说ACTION_POINTER_UP
,在这个时机之后,下一个事件(例如ACTION_MOVE
)发生前,我们必须取到在下一个事件起作用的那个手指的坐标,很显然,只要找到除开松开的那根手指外,剩余手指中ID最小的那个手指,问题就可以解决了;然后我们再来看看ACTION_POINTER_DOWN
,同理需要找到包括新加入的手指的所有手指中ID最小的那根,将其坐标更新至lastXY
。
具体代码演示如下:
1 | private float lastX, lastY; |
上述代码的注释已经很清楚了,可以看到,它和原来的代码相比其实并没有增加几行代码,但是那些丑陋的视觉问题却得到了解决。其实,还有许多由多手指引发的不连续的效果的问题,都可以用上述“万金油”方案原理来解决,在此就不一一列举了。当然,更新lastXY的方式也并非此一种,只要理清了原理,便能在实际业务中灵活地处理。
尾语
本文通过一个简单的案例来归纳总结了关于Android多手指Touch变化处理原则的基础知识,对多指缩放、旋转等效果的实现是一个很好的参考。