1、前言
简单说一下安卓屏幕是如何感知到手指触摸的,目前安卓手机屏幕绝大部分都是电容屏,屏幕上覆盖着一层导电层,当手指触摸屏幕时,由于人体是导电的,所以触摸点的电容会发生变化,屏幕上的电容传感器就能感知到,从而可以计算得到触摸点的坐标。
2、安卓输入事件类型
安卓系统里面的输入事件大致分为两类按键事件和动作事件,即KeyEvent 和 MotionEvent,
按键事件:顾名思义,就是各种虚拟按键或者实体按键按下后触发的事件,比如以前手机上带26个字母的实体键盘,底部的三个导航栏按键(菜单键,home键,返回键),输入法的虚拟键盘等;
动作事件:可以简单理解为触摸事件,即手指触摸屏幕后触发的一系列事件,不过也有例外,蓝牙手柄的矢量扳机键和矢量方向键以及左右摇杆也算动作事件。
3、事件分发原理
我们以两个简单的场景为例,分析一下事件的产生和分发过程:
① 当我们手指快速点击一下屏幕时:这个过程会产生两个事件,手指刚触碰屏幕产生的DOWN事件,以及手指离开屏幕时产生的UP事件,这种情景下和按键事件类似,都是简单的按下和松开两个事件,这两个事件为一组完整的动作。
② 当我们手指按住屏幕并移动一段距离后再松开:这个过程会产生多个事件,触碰屏幕时的DOWN事件,手指移动时产生的多个MOVE事件(这个的数量会很多,可以简单理解为你手指移动画出了一条线,但是安卓系统用多个的取样点去还原你这条线,Ps:一个MOVE事件其实也是将多个点的坐标打包成一个事件的),离开屏幕时产生的UP事件,这一系列事件为一组完整的动作。
聪明的你肯定能发现一组完整的动作一定是以DOWN事件开始,以UP事件结束的。
下面说下事件的分发机制,说分发机制前,先提一下安卓视图的层级结构,下图中最里面的ContentView就是我们自定义的各种页面,里面包含各种ViewGroup和View
我们简化一下上图中的层级关系,日常开发中最常见的场景就是 Activity---ViewGroup---View,
3.1 安卓系统分发的逻辑也并不复杂,当系统拿到触摸事件后,在没有人消耗这个事件的前提下,会先传递给最外层的Activity,然后Activity往下传递给ViewGroup,ViewGroup再传递给View,如果View不消耗这个事件,这个事件又会一层一层往上传回去,事件最后会被传回给Activity,如图三所示:
图三
图三中Activity有两个回调方法可以拿到这个事件,dispatchTouchEvent 和 onTouchEvent;
ViewGroup有三个回调方法可以拿到这个事件,dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent;
View有三个回调方法可以拿到这个事件,dispatchTouchEvent 、onTouch(该方法需要主动设置监听) 和 onTouchEvent
每个回调方法里面如果返回 true,代表消耗该事件,不再往下传,后续同一组动作的事件也不再往下传;
如果返回false,代表不消耗该事件,继续往下传。
3.2 如果在ViewGroup中的onInterceptTouchEvent(该方法是安卓系统专门提供的拦截入口) 中返回true进行拦截,那么事件将直接传递给ViewGroup的onTouchEvent方法,此时需要在这个方法里对事件自行处理,同时同一组动作的后续其他事件也不再经过onInterceptTouchEvent,而是直接从dispatchTouchEvent 到onTouchEvent,如图四所示,
图四
3.3 当然你也可以在View中消耗事件,View中有两种情况:
一个是在onTouch里面返回true,然后在onTouch里面自行处理事件,如图五;
另一个是在onTouchEvent里面返回true,然后也在onTouchEvent面自行处理事件,就不再赘述;
图五
4、多点触控
安卓系统的多点触控主要涉及以下五个事件,DOWN,POINTER_DOWN,MOVE,POINTER_UP,UP,其中我们主要关注POINTER_DOWN 和 POINTER_UP,这两个事件只能通过getActionMasked方法才能判断出来,所以多点触控处理时,应使用getActionMasked,而不是getAction,具体原因后面会讲。
POINTER_DOWN: 当按下时,屏幕上已经有手指,会触发该事件
POINTER_UP:当抬起时,屏幕上还有其他手指,会触发该事件
再正式讲多点触控之前,还得明确两个概念:
ActionIndex:事件下标,表示当前事件在屏幕上的排列顺序,这里可以理解为表示触发该事件的手指在屏幕上的顺序序号
PointerID:触摸点ID,触摸点的唯一标识,只要当前手指还没有离开屏幕,这个ID就能作为手指的唯一标识,用来追踪不同手指的移动轨迹
下面用几张图来介绍多点触控的逻辑:
情景一:从大拇指到无名指四根手指依次在屏幕上按下,再按原顺序依次抬起
图六
图六中箭头代表用户动作,方框里面代表对应动作触发的事件的各项参数值
我们先看ActionIndex的值,当手指依次按下时,这个值从0到3,就是手指按下的顺序,当抬起时,我们发现ActionIndex一直是0,你可能有点疑惑,但其实很好理解,前面我们说了ActionIndex这个值就是代表事件在屏幕上的排列顺序,所以,当大拇指没有抬起之前,大拇值排第一个,当大拇指抬起后,食指就排第一个了,以此类推,所以ActionIndex一直是0了。
然后我们再看PointerID这个值,前面也说了,他是表示触发这个事件的手指的唯一标识,只要手指没有离开屏幕,这个值就一直不变。
最后我们来看getAction这个值,这个值就是指的MotionEvent.getAction()这个方法返回的值,先提前说下,上图六中的步骤1和步骤8触发的是DOWN和UP事件,而步骤2到步骤7触发的是POINTER_DOWN和POINTER_UP事件,所以我们说下POINTER_DOWN和POINTER_UP这两个事件的getAction值是怎么算的,直接上结论:
POINTER_DOWN的getAction = ActionIndex * 16*16+5(其实就是左移八位再加5)
POINTER_UP的getAction = ActionIndex * 16*16+6
情景二:从大拇指到无名指四根手指依次在屏幕上按下,再按倒序依次抬起
图七
图七中,我们可以看到抬起时,ActionIndex的值没有变,如果图六理解了的话,这里不变就是因为是从后往前依次抬起的,没有打乱前面的顺序,所以值没有变。
情景三:一个稍微复杂点的场景
图八
我们直接看图八中的步骤6,食指抬起后,中指的ActionIndex为1这个好理解,步骤7小拇指按下时,我们看到它的ActionIndex为1,PointerID为1,后者为1是因为复用了前面回收的PointerID,由于源码中getaction值是native层计算的,不方便查看,ActionIndex为1我猜测是存储PointerID的数组在加入1后,又按从小到大的顺序进行了重排序,然后新的数组下标就作为了ActionIndex,所以我们在实际应用中,应该使用PointerID追踪多指触摸事件。
简单总结下:ActionIndex由于是索引,一直是连续的,在抬起和放下时会变 ,而PointerID不会变。所以ActionIndex用于遍历,id用于多指行为追踪。
在ACTION_MOVE时遍历所有pointer拿到PointerID:
for (int i = 0; i < event.getPointerCount(); i++) { pointerId = event.getPointerId(i); //TODO }
也可以通过event.findPointerIndex(pointerID)获取Index值;
另外多指触控中,会把多个触摸点的坐标打包成一个MOVE事件,所以我们需要通过上面提到的PointerID和ActionIndex拿到对应点的坐标值,如下图所示
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
//遍历当前屏幕上的手指
for (int i = 0; i < event.getPointerCount(); i++) {
//通过下标拿到手指ID
int pointerID = event.getPointerId(i);
if (pointerID == targetID) {
//遍历MOVE事件里面存的坐标数据
for (int j = 0; j < event.getHistorySize(); j++) {
//触摸点坐标
PointF pointF = new PointF();
//i表示pointerIndex,也就是上面说的actionIndex,触摸点的顺序序号
//j表示该触摸点有几条坐标数据,因为MOVE事件不一定是一个坐标点,可以是多个坐标点的集合
pointF.set(event.getHistoricalX(i, j), event.getHistoricalY(i, j));
}
}
}
break;
}