安卓输入事件分发与多点触控机制解析

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有三个回调方法可以拿到这个事件,dispatchTouchEventonInterceptTouchEvent 和 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_DOWNPOINTER_UP事件,所以我们说下POINTER_DOWNPOINTER_UP这两个事件的getAction值是怎么算的,直接上结论:

POINTER_DOWNgetAction = ActionIndex * 16*16+5(其实就是左移八位再加5)

POINTER_UPgetAction = 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;
        }

  • 24
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值