Android触摸事件(一)-TouchEventHelper

目录

  • 目录
    • 概述
    • 关于更新
      • 2016-08-31
      • 2016-06-20
    • 关于单点触摸事件singleTouch
      • 单击的两种方式
      • 关于双击事件
        • 双击事件的检测逻辑
        • 双击事件触发的时机
    • 关于多点触摸事件multiTouch
      • 两点触摸中的移动事件
      • 两点触摸事件的触发过程
      • 两点触摸的事件
    • 实现
      • 变量定义
      • 触摸事件流程
        • 自定义事件计时方案
        • 自定义事件触发区域
        • 触摸事件处理规则
        • 关于回调的方法
        • 触摸事件处理源码
        • 双击事件的优化处理
          • 如何检测当次触摸事件的单击事件
          • 如何检测触发双击事件
        • 辅助补充逻辑
    • 小结
    • 使用方式
    • 源码
    • GitHub地址

概述

这是一个 触摸 事件统一处理辅助类;处理的主要是点击事件,其中包括了:

  • 单点触摸事件
  • 多点(两点)触摸事件

此类可以处理的事件包括:

  • 单击事件(基于时间与距离的两种单击事件,详见下文)
  • 双击事件
  • 单点触摸移动事件(可用于实现界面拖动)
  • 多点触摸移动事件(可用于实现界面缩放)
  • 所有触摸事件的相应回调(down/move/up事件)

此类实现View.onTouchListener,通过此监听方法实现对触摸事件的操作.在Activity中也可以使用,调用时onTouch(View,MotionEvent)传递的参数中将view设置为null即可.
整个触摸事件处理中并不会涉及任何跟view有关的操作,仅分析及处理MotionEvent


关于更新

2016-08-31

修正部分逻辑,添加了双击是否可用的设置.修改了文章中一些相应的部分及说明.

2016-06-20

AbsTouchEventHandle抽象类修改为TouchEventHelper的辅助类,将抽象方法抽出到接口OnToucheEventListener中,通过为helper设置对应的事件处理接口即可直接处理事件.
修改为helper的辅助类之后,就需要通过调用其方法来处理触摸事件.TouchEventHelper实现了View.onTouchListnener接口,所以对事件的处理需要通过以下的方式调用.

//参数为事件处理接口 onTouchEventListener
TouchEventHelper helper=new TouchEventHelper(this);
//直接在需要处理触摸事件的地方调用onTouch方法,如:
//onTouchEvent(MotionEvent event),在view中
//dispatchTouchEvent(MotionEvent event),在view,activity或者viewGroup中
helper.onTouch(null,event);
//或者直接使用view.setOnTouchListener(helper)
//activity中无法使用setOnTouchListener(),所以只能在对应的方法中进行调用

关于单点触摸事件(singleTouch)

单点触摸事件很好理解,触发的流程一般是:

mouse_down -> mouse_move -> mouse_up

这个过程可能发生的事件有以下几种:

  • 单击事件
  • 单点移动事件
  • 双击事件

但是必须注意的点是:

单击事件可能触发mouse_move事件,而单点移动必定触发mouse_move事件

在单击事件中,mouse_move事件并不是百分百会触发的,触摸的时候先触发的是mouse_down事件,如果触摸的时间足够长(按住不动时),接下来会触发mouse_move事件,之后抬起时会触发mouse_up事件
虽然触发了mouse_move事件(按住不动),但是这依然是一个单击事件,如果进行调试或者输出移动的距离,可以明显得到距离为 0


单击的两种方式

  • 基于时间的单点触摸事件(singleTouchByTime)

以时间来计算单击事件时,这个过程可以不必过多地考虑单击可能触发的mouse_move事件,因为单击本身就是一个时间足够短的操作,即便存在一定小范围的移动偏差也是允许的,当然这种情况是在 时间足够短 的情况下
我们可以这么处理:

//定义全局变量用于存放按下时的时间点
long downTime=0;
switch(event.getAction()){
    case MotionEvent.ACTION_DOWN:
        //触摸时记录当前时间
        downTime=System.currentTimeMillis();
        break;
    case MotionEvent.ACTION_UP:
        //抬起时计算与按下时时间的差
        long tillTime=System.currentTimeMillis()-downTime;
        //时间差在允许范围内时,视为一次单击事件成立
        if(tillTime<150){
            //处理单击事件
        }
        //否则不视为一次单击事件
        break;
}

通过计算按下时与抬起时的时间差来确定是否是一次单击事件(150ms足够了),这是基于时间的单击事件;


  • 基于距离的单点触摸事件(singleTouchByDistance)

从上面我们知道单点触摸时也是可能触发mouse_move事件的,所以

mouse_move事件并不能作为一个是否单点移动的标识,实际上,多点触摸的移动也会触发mouse_move事件

而且我们已经知道了单击也可以是按住某个位置不动,持续一段时间之后再抬起,此时可能时间上已经达到一个足够长的时间,但其实点击地方的坐标并没有改变,这种情况下我将其也视为单击的一种情况(总会在某些情况下需要处理这种单击方式)

参考 基于时间的单击方式 的处理方法,我们可以得到类似的处理方法:

float downX=0;
float downY=0;
switch(event.getAction()){
    case MotionEvent.ACTION_DOWN:
        //触摸时记录当前触摸点的坐标
        downX=event.getX();
        downY=event.getY();
        break;
    case MotionEvent.ACTION_UP:
        //抬起时计算与按下时坐标的偏移距离
        float offsetX=Math.abs(event.getX()-downX);
        float offsetY=Math.abs(event.getY()-downY);
        //偏移量差在允许范围内时,视为一次单击事件成立
        if(offsetX<20 && offsetY<20){
            //处理单击事件
        }
        //否则不视为一次单击事件
        break;
}

以上为两种单击方式的处理方式


关于双击事件

双击事件的检测逻辑

由于单击事件存在两种不同情况,所以双击同理衍生出两种方式, 基于时间和基于距离两种双击事件
不管是哪种方式,原理都是一样的,基于对应的单击方式实现第一次单击,两次单击事件就构成了一次双击事件;
同时这里存在一个问题是,双击不管从哪个角度来说,都是指两次时间间隔短暂的单击事件,所以不管是基于时间还是基于距离的双击事件,都是以两次单击时间之前的间隔时间不超过某个范围来确定一次双击事件的.

插个小话题
----------
基于距离的双击事件其实也是可以不按时间来处理的,只要两次单击事件的距离在一定的偏移值范围内,可认为是一次双击事件(与时间无关);
但此方式存在的问题是,如果是两次连接发生在同一个位置的单击事件,此时就无法正确的区分出到底是一次双击事件还是两次单击事件了.所以并不推荐使用此方式处理,而是按两次单击事件间隔在一定时间差内视为一次双击事件

由上可以看出,其实这里的双击事件构成该双击事件的单击事件可能是 基于时间的或者是基于距离的 单击事件

//用于记录是否已经完成一次单击事件
boolean isSingleClick=false;
switch(event.getAction()){
    //忽略ACTION_DOWN逻辑
    case MotionEvent.ACTION_UP:
        //达成一次单击事件操作时,视为一次单击事件成立
        if(singleClickFinish){
            //判断是否已经完成了一次单击事件(在允许的双击间隔时间内)
            if(isSingleClick){
               //若已完成了一次单击事件,此次单击构成了双击事件
                //处理双击事件
            }else{
                //仅为一次单击事件
                //处理单击事件
                //记录已经完成了一次单击事件
                isSingleClick=true;
            }
        }
        //否则不视为一次单击事件
        break;
}

同时,这里有一个需要注意的地方是,双击事件本质是两次单击事件构成的,第一次单击事件发生时我们无法确定是否是一个正常的单击事件还是可能会构成一次双击事件,所以必须按正常单击事件响应;

但第二次单击事件发生时,我们已经可以确定构成了一次双击事件,此时不应该再响应单击事件,而应该优先响应双击事件,且一旦响应了双击事件,就应该结束整个触摸事件.

实际的处理事件并没有这么简单,以上是简单的处理逻辑,具体的实现请参照下文 双击事件的优化处理


双击事件触发的时机

双击事件触发的时机是比较重要的.因为双击事件是由单击事件触发的.必然先检测单击事件之后再检测双击事件;
但一旦单击事件被触发了,那么接下来需要做的操作有两个选择:

  • 检测双击事件(之后执行双击事件)
  • 或执行单击事件

这两个事件的优先性是必须确定的而且会造成不同的影响.
如果先执行单击事件,则可能会造成在后续双击事件成立的之前,单击事件会被执行一次.这并不合理,也可能存在一些不安全的因素(如果单击操作会影响到双击操作的情况下)

因此应先检测双击事件,一旦双击事件成立,直接执行双击事件,同时忽略单击事件;(用户触发了双击事件本身包含了不需要执行单击事件的想法,否则直接触发单击事件即可)

这也是为什么事件触发规则会双击事件优先;


关于多点触摸事件(multiTouch)

多点触摸事件相对比较复杂,此处只讨论 两点触摸.
多点触摸事件的需要通过额外的方式进行检测并处理事件,无法与单点触摸事件一样直接event.getAction()得到的就是相关的触摸事件;

//分离触摸事件,使用 MotionEvent.ACTION_MASK
//此方式可以正确分离出多点触摸事件 ACTION_POINTER_X,也可以正常返回单点触摸事件 ACTION_X
switch(event.getAction() & MotionEvent.ACTION_MASK){
    case MotionEvent.ACTION_X:
        //单点触摸事件处理
        break;
    case MotionEvent.ACTION_POINTER_X:
        //多点触摸事件处理
        brea;
}

两点触摸中的移动事件

首先,必须注意的一个点是:
多点触摸事件中移动时触发的移动事件也是ACTION_MOVE

也就是说ACTION_MOVE事件是移动的通用事件,在单点触摸移动和多点触摸移动中都存在.


两点触摸事件的触发过程

除以上提及的共用ACTION_MOVE事件之外,多点触摸事件可能存在的过程是这样的:

  • [x] 情况1
`ACTION_DOWN` -> `ACTION_POINTER_DOWN` -> `ACTION_MOVE` -> `ACTION_POINTER_UP` -> `ACTION_UP`

这种情况是在两点触摸时,两个手指刚好 同时按上 -> 移动 -> 同时抬起

很明显,既然存在同时触摸,也肯定存在非同时触摸了.当非同时触摸时的过程是这样的:

  • [x] 情况2
`ACTION_DOWN` -> `ACTION_MOVE` -> `ACTION_POINTER_DOWN` -> `ACTION_MOVE` -> `ACTION_POINTER_UP` -> `ACTION_MOVE` -> `ACTION_UP`

这种情况是先单点触摸,触发了ACTION_DOWN事件,然后第二个触摸点按下时,触发ACTION_POINTER_DOWN,然后当触摸点抬起时,触发ACTION_POINTER_UP(多个触摸点的情况下会多次触发ACTOIN_PONTER_DOWNACTION_POINTER_UP),之后单点触摸抬起,触发ACTION_UP;

  • 至于第一个ACTION_MOVE事件是否会触发取决于第一个触摸点与第二个触摸点之间的时间间距(如果第二次按下的时间与第一次按下时间间隔足够短,则不会触发);

  • 同理第二个ACTION_MOVE取决于多点触摸的按下与抬起的时间差,类似于单击,多点触摸按住时,ACTION_MOVE依然是正常触发,但距离值还是 0.

  • 而第三个ACTION_MOVE是所有多点触摸抬起后(只剩下单点触摸时),若还保持单点触摸(不管有没有移动)就会触发第三轮的ACTION_MOVE事件

以上过程可以明确得到:
不管是同时多点触摸还是间接多点触摸,ACTION_DOWNACTION_UP两个事件是必定会触发并永远在第一项和最后一项事件

所以,在处理多点触摸的事件时,必须小心处理ACTION_MOVEACTION_UP事件,因为这两个事件并不是单点触摸专属的事件,而是所有的触摸事件都会触发的


两点触摸的事件

在两点触摸时,一般我们不考虑两点”单击”(ACTION_POINTER_DOWN)事件,主要是针对两点触摸时触发的移动事件进行处理;往往这种情况需要处理的是类似放大/缩小的功能

  • 如何判断两点触摸事件并处理

对于两点触摸事件,这个很好判断;当ACTION_POINTER_DOWN触发时,说明触发了两点触摸事件;当ACTION_POINTER_UP触发时,说明两点触摸事件结束; 两点触摸事件主要是基于这两个事件之间,难点在于:

如何区分单点触摸的移动事件和两点触摸的移动事件

根据以上我们确定两点触摸时,会触发ACTION_PONTER_DOWN事件,之后才会触发两点触摸事件的ACTION_MOVE,因此可以通过此事件确定当前的ACTION_MOVE事件是否属于两点触摸的还是单点触摸的事件

boolean isMultiDown=false;
switch(even.getAction() & MotionEvent.ACTION_MASK){
    case MotionEvent.ACTION_POINTER_DOWN:
        //记录多点触摸事件触发
        isMultiDown=true;
        break;
    case MotionEvent.ACTION_MOVE:
        //检测是否已经触发了多点触摸事件
        if(isMultiDown){
            //多点触摸移动事件
        }else{
            //单点触摸移动事件
        }
        break;
}

第二个可能的难点在于:

如何在ACTION_UP事件中区分并处理多点触摸事件及单点触摸事件

一般来说,处理多点触摸事件时只关注多点触摸事件;处理单点触摸事件时只关注单点触摸事件;而两者都存在的ACTION_UP事件并且都在最后,就可能造成一个不必要的麻烦:

可能在多点触摸事件结束后,触发的ACTION_UP事件处理了一次单点触摸事件

而这可能会导致某些我们不想要的情况发生.所以关于ACTION_UP事件,我们需要小心处理.当然,两点触摸的抬起事件是很明确的ACTION_POINTER_UP,如果仅仅关心两点触摸事件时,就完全不需要再考虑ACTION_UP事件了.(不过在此辅助类中需要在up事件中回调单击事件,所以这部分的处理还是需要的.)


实现

根据以上的说明,大致的一个触摸事件流程和需要响应的事件也已经确定下来了.下面是整个触摸事件及流程的一个实现思路,包括:

  • 变量定义
  • 触摸事件处理流程

变量定义

因为触摸事件各种情况相对复杂,先确定需要处理的事件包括如下事件:

以下事件为触摸事件,提及触摸事件特指以下五种系统反馈的触摸事件,单击/双击等非系统预定义事件称为自定义事件

  • ACTION_DOWN
  • ACTION_POINTER_DOWN
  • ACTION_POINTER_UP
  • ACTION_UP
  • ACTION_MOVE

需要的变量包括:

//是否单点触摸按下,用于单击事件的检测
boolean isSingleDown;
//是否多点触摸按下,用于区分处理事件
boolean isMultDown;
//多点触摸按下的次数,对应多点触摸的个数
//尽管我们只处理两点触摸,但实际可能多达N点触摸
int multiTouchCount;
//是否进行了移动,不区分多点移动还是单点移动
boolean isInMotionMove;
//是否进行了单点触摸移动(优化处理单点触摸与多点触摸的切换)
boolean isSingleMove;
//是否完成一次单击事件
boolean isSingleClikEvent;

以上为主要的需要变量,用于记录各种不同的状态以区分多点触摸与单点触摸及之间的单击/双击/移动事件


触摸事件流程

自定义事件计时方案

首先需要知道的一个事情是:
关于单击/双击事件中时间间隔的计时我们通过Handler来处理

由于Hanlder可以发送Delay的消息,我们可以通过指定发送延时的消息交给Handler去取消事件或者消费事件;如下例子:

Hanlder mHandler=new Handler{
    @Override
    public void handleMessage(Message msg){
        if(msg.what==CANCLE_EVENT){
            //处理对应的消息取消操作
        }
    }
}
//250ms后发送取消事件消息
mHandler.sendEmptyMessageDelayed(CANCLE_EVENT,250);

通过此方法,我们可以在ACTION_DOWN事件触发后设置一个按下的标识,然后发送一个延迟的取消按下事件消息,在ACTION_UP中直接检测按下事件标识是否有效,有效则达成一次单击事件,无效则说明已经超时,单击事件无法触摸;

必须注意:不管ACTION_UP事件中按下标识是否有效,已经发生了ACTION_UP必定发生过ACTION_DOWN,按下识别是作为单击事件的检测标识而不是ACTION_DOWN的触发标识


自定义事件触发区域

对于以上提及的不同的触摸事件中,不同的事件可能会触发不同的自定义事件

  • 基于时间的单击事件:由ACTION_DOWN/ACTION_UP事件触发
  • 基于距离的单击事件:由ACTION_DOWN/ACTION_MOVE/ACTION_UP事件触发
  • 双击事件:由ACTION_DOWN/ACTION_UP事件中被触发

触摸事件处理规则

  • 任何时候触发双击事件不再响应其它事件(单击或者UP等其它事件)
  • 任何时候触发多点触摸事件则不再响应单点触摸的MOVE事件

对于每个触摸事件,除非被其它事件消费或者拦截(如双击事件会拦截其它后续事件),否则都会进行一次回调提供给子类进行处理,其中

  • ACTION_DOWN/ACTION_UP
    回调事件:onSingleTouchEventHandle(MotionEvent,int)

  • ACTION_POINTER_DOWN/ACTION_POINTER_UP
    回调事件:onMultiTouchEventHandle(MotionEvent,int)

  • ACTION_MOVE比较特殊,存在两个回调可能

    1. 单点触摸移动事件回调:
      onSingleTouchEventHandle(MotionEvent,int)
    2. 多点触摸移动事件回调:
      onMultiTouchEventHandle(MotionEvent,int)

关于回调的方法

统一回调的方法分为单点触摸事件回调,多点触摸事件回调;回调的时机是每一个对应的MotionEvent触发时,在处理所有事件(单击双击等)之后都会回调对应的事件以通知子类自定义处理.

  • onSingleTouchEventHandle(MotionEvent, int)//单点触摸事件回调
  • onMultiTouchEventHandle(MotionEvent, int)//多点触摸事件回调

各事件的回调对应如下:

//省略参数
switch(event.getAction() & MotionEvent.MASK){
    case MotionEvent.ACTION_DOWN:
        onSingleTouchEventHandle();
        break;
    case MotionEvent.ACTION_POINTER_DOWN:
        onMultiTouchEventHandle();
        break;
    case MotionEvent.ACTION_UP:
        onSingleTouchEventHandle();
        break;
    case MotionEvent.ACTION_POINTER_UP:
        onMultiTouchEventHandle();
        break;
    case MotionEvent.ACTION_MOVE:
        //move事件是共用的,所以需要区分回调事件的类型
        onSingleTouchEventHandle() || onMultiTouchEventHandle();
        break;
}

参数意义:

  • 参数1为: 触摸事件
  • 参数2为: 建议处理的触摸事件类型

参数1很好理解,只是传送了系统分发的触摸事件变量而已,包括了触摸点的坐标,触摸状态等;
参数2是一个比较关键的参数;其存在是的意义是,建议以某个事件去处理当前的事件而不是直接按触发的事件处理当前的事件.其使用的场景如下

由于任何时候触发了多点触摸事件则不再处理单点触摸事件的MOVE事件(触发规则)
所以当多点触摸事件ACTION_POINTER_DOWN发生之后,所有的ACTION_MOVE转为多点触摸的移动事件;
因此如果之前存在单点触摸的ACTION_MOVE事件时,将结束该事件回调onSingleTouchEventHandle()并不再处理;

因此,此时回调该事件时,将通知子类处理事件时建议处理为ACTION_UP事件(因为从这个时候开始整个单点触摸事件已经结束了,之后也不会再响应任何单点触摸事件),视为一次单点触摸事件的结束


  • 注意事件

这里回调时只是建议而不会修改MotionEvent的事件参数,子类可以选择忽略.
另外建议处理为ACTION_UP的原因是,单点触摸事件结束的标志是ACTION_UP,很可能子类需要在这个时候处理某些数据或者保存工作.

由于切换为多点触摸之后不再响应单点触摸事件,而最终事件结束时的ACTION_UP事件中的参数也很可能与此时的参数不一致(最主要的就是触摸点的坐标了),因此此回建议处理为ACTION_UP事件.

举例:
如进行一次单点移动操作时,再按下一个触摸点就变成了两点触摸事件.此时所有的事件将转变成两点触摸事件,因此单点触摸事件就结束了,回调action_up(建议的操作)通知相关的保存工作.
若不这样处理,两点触摸中任何的事件都可能影响到界面的变化,而且移动事件也变得不清晰了,所以在整个触摸事件结束时才回调action_up的话,很有可能会让单击操作的很多参数出现问题.

以下为ACTION_MOVE事件中区分单点触摸及多点触摸事件的操作.

case ACTION_MOVE:
    //若已经触发了多点触摸事件且保持在多点触摸状态
    //当 multiTouchCount=0 时说明已经退了多点触摸状态,恢复到单点触摸状态
    //但之后依然不会响应单点触摸的 MOVE 事件
    if (mIsMultiDown && mMultiTouchCount > 0) {
        //若此前是单点触摸的移动状态时
        if (mIsSingleMove) {
            //按单点触摸的结束状态处理并不再响应单点触摸移动状态
            showMsg("单击 move 结束");
            //结束单点触摸事件,并建议处理为 UP 事件
            this.onSingleTouchEventHandle(event, MotionEvent.ACTION_UP);
            mIsSingleMove = false;
        }
        //正常直接多点移动操作
        showMsg("多点触控 move");
        this.onMultiTouchEventHandle(event, MOTION_EVENT_NOTHING);
    }
break;

此处的实际应用场景在于:当界面被拖动移动时(依赖于ACTION_MOVE事件),切换到多点触摸状态时可以保证界面的正常(此时依赖于ACTION_UP事件保存移动后的位置)而不会在触摸事件结束时再保存移动后位置,可能会发生突然转到移动到某个位置的情况


触摸事件处理源码

以下为触摸事件处理的完整流程(还有其它的处理逻辑,但属于辅助性的逻辑)

switch (event.getAction() & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
        //进入单点单击处理
        showMsg("单点触摸 down ");
        mIsSingleDown = true;
        //发送延迟取消按下标识消息
        mHandle.sendEmptyMessageDelayed(HANDLE_SINGLE_DOWN, SINGLE_CLICK_INTERVAL);

        //记录按下坐标
        mDownX = event.getX();
        mDownY = event.getY();
        this.onSingleTouchEventHandle(event, MOTION_EVENT_NOTHING);
        break;


    case MotionEvent.ACTION_POINTER_DOWN:
        //开始多点单击事件
        showMsg("多点触控 down");
        mIsMultiDown = true;
        //每发生一次多点触摸此事件会触发一次
        //通过此事件可以记录多点触摸的次数及判断是否已经退出多点触摸状态(当变量为0时)
        mMultiTouchCount += 1;
        this.onMultiTouchEventHandle(event, MOTION_EVENT_NOTHING);
        break;


    case MotionEvent.ACTION_UP:
        showMsg("单点触摸 up");
        //任何一种事件中,只要触发了双击事件,则结束事件
        //TODO: 双击事件检测并处理,触发 break;否则执行单点触摸抬起事件

        //在处理单击事件up中,任何时候只要在结束up之前产生任何的多点触控,都不将此次的事件处理为单点触摸up
        //因为这时候单点触摸事件已经不完整了,混合了其它的事件且多点触摸可能导致原本的单点触摸事件的坐标数据不正常,所以不再处理单点触摸事件
        if (!mIsMultiDown && mMultiTouchCount <= 0) {
            //此处分为两种情况
            //一种是未进行任何多点触摸状态的,那么必定为单,事件必须响应
            //在事件响应处两个判断条件是:1.用户快速单击,没有move事件,此时 isInMotionMove=false;
            if (!mIsInMotionMove
                    //2. 用户慢速单产生了move事件但仍没有造成多点触摸事件,此时 isInMotionMove=true 且 isSingleMove=true;
                    || (mIsInMotionMove && mIsSingleMove)) {
                showMsg("单击 up");
                this.onSingleTouchEventHandle(evenMOTION_EVENT_NOTHING);
            } else {
        //一种是进行了多点触摸,且在多点触摸结束之后保持单点触摸的状态,此时以多点触摸按下的时刻处理触摸事件(即在move中已经按up处理掉事件了)
        //则在完成所有事件之后的up中将不再处理该事事件,即下面的"不处理"
                showMsg("单击 up 不处理");
            }

        //处理触摸结束事件,重置变量
        this.finishTouchEvent();
        break;


    case MotionEvent.ACTION_POINTER_UP:
        //当确认进入多点单击状态,则执行多点单击抬起事件
        if (mMultiTouchCount > 0) {
            showMsg("多点触控 up");
            this.onMultiTouchEventHandle(event, MOTION_EVENT_NOTHING);
        }
        //每次多点触摸抬起触发一次,多点触摸次数-1(直到只剩单点触摸为止不再触发此事件,此时变量值为0)
        mMultiTouchCount -= 1;
        break;


    case MotionEvent.ACTION_MOVE:
        //进入移动状态
        mIsInMotionMove = true;
        //当前不是多点单击状态,则进行移动操作
        //若触发了多点触摸事件,则结束单点移动事件,进入多点触摸移动事件

        //结束单点移动操作后在触摸事件结束之前都不会再执行单点移动操作
        //这种情况是为了避免有可能有用户单击移动之后再进行多点触控,这种情况无法处理为用户需要移动还是需要缩放
        //而且引起的坐标变化可能导致一些错乱
        if (!mIsMultiDown && mMultiTouchCount <= 0) {
            showMsg("单点触摸 move");
            this.onSingleTouchEventHandle(event, MOTION_EVENT_NOTHING);
            mIsSingleMove = true;
            //多点触摸事件触发了,进入多点触摸移动事件
        } else if (mIsMultiDown && mMultiTouchCount > 0) {
            //若此前是单点触摸的移动状态时
            if (mIsSingleMove) {
                //按单点触摸的结束状态处理并不再响应单点触摸移动状态
                showMsg("单击 move 结束");
                this.onSingleTouchEventHandle(event, MotionEvent.ACTION_UP);
                mIsSingleMove = false;
            }
            //正常直接多点移动操作
            showMsg("多点触控 move");
            this.onMultiTouchEventHandle(event, MOTION_EVENT_NOTHING);
        }
        break;
}

双击事件的优化处理

前面提到,关于双击事件的处理逻辑是这样的:完成一次单击事件记录单击事件标识,第二次触发单击事件时,根据此前是否存在第一次单击事件来确定是否触发双击事件

在这里需要注意的一个点是,单击事件存在两种方式,而每一种方式的完成都是一次单击事件.
我们定义的双击事件是:

两次一定时间间隔内连续发生的单击事件即为一次双击事件,这里单击事件的触发方式是任意的

而由于单击事件在ACTION_UP事件中检测并触发,两种方式的单击事件都需要检测及处理,所以这个过程可能导致单击事件会被触发两次

  • 基于时间的单击事件触发一次
  • 基于距离的单击事件触发一次

这种情况下就需要用不同的变量来识别单击事件的触发了.需要处理的包括:

  • 是否已经触发了一次单击事件(即上一次单击事件是否还在有效时间间隔内)
  • 当次触摸事件中是否触发了单击事件(不管该事件由哪种方式触发)

如何检测当次触摸事件的单击事件

由于两种方式的单击事件都需要检测一次,所以可能存在一种情况:(不管哪种方式优先检测)

条件:假设单击事件成立标识为true
情况:
-----
1.假设已经进行了一次单击,最后一次单击标识为true.
2.首先检测第一种单击方式(先检测是否触发双击事件),当第一种方式单击成立之后,本次触摸事件单击事件成功,标识置为true;
3.当第二种方式检测时(先检测是否触发双击事件),最后一次单击标识为true,且本次触摸事件已经为true(上一个方式已经确定),则会触发一次双击事件
-----
结果:在一次单击事件检测中,由于两种单击方式都需要检测就可能导致一次单击事件就会触发双击事件了.
这个过程明显是存在BUG

因此需要设置不同标识在不同方式的单击事件中使用;
当然,现在已经添加了两种检测方式只能触发一种的处理,所以这个问题容易处理很多.

//是否触发基于时间的单击事件
boolean isFireTimeClickEvent=false;
//是否触发基于距离的单击事件
boolean isFireDistanceClickEvent=false;
//上一次单击事件是否被触发了
boolean isFireLastClickEvent=false;

如何检测触发双击事件

双击事件是基于单击事件的,两次连续触发的单击事件才构成一次双击事件;因此双击事件的检测(这里需要注意事件的检测与事件的响应是分开的,检测到某个事件不一定需要响应对应的事件):

  • 必须在单击事件检测之后才检测双击事件(触发了单击事件才可能构成一次双击事件)
  • 双击事件的检测必须优先于单击事件的响应(一旦触发双击事件不再响应单击事件)
  • 一旦双击事件被触发,则忽略其它所有事件(也不再响应单击事件)

任何一次单击事件检测完毕之后都需要检测双击事件,再执行单击事件;单击事件需要检测两次(因为有两种触发方式),因此双击事件是必须检测两次的.
双击事件被触发时,其它事件ACTON_UP事件是不会触发的

//检测是否触发双击事件
//检测本次触摸事件中是否触发了(任何一种)单击事件
if((isFireTimeClickEvent || isFireDistanceClickEvent) 
    //检测上一次单击事件是否触发了
    && isFireLastClickEvent){
    //条件成立,触发双击事件
    //同时重置所有相关变量
    //因为双击事件已经触发,保留变量状态会影响下一次判断
    isFireTimeClickEvent=false;
    isFireDistanceClickEvent=false;
    isFireLastClickEvent=false;
    //处理了双击事件则不再响应任何其它事件
    break;//或者return;
}
//若处理了时间单击事件,对应标识置为true
isFireTimeClickEvent=true;

//同上检测双击事件
//若处理了距离单击事件,对应标识置为true
isFireDistanceClickEvent=true;

//保存此次单击状态
isFireLastClickEvent = isFireTimeClickEvent || isFireDistanceClickEvent;

辅助补充逻辑

在以上的触摸处理事件中,我们提到单击分为两中方式:

  • 基于时间的单击事件
  • 基本距离的单击事件

基于时间的单击事件处理已经在ACTION_DOWN事件中操作了,通过延时发送取消按下标识,再从ACTION_UP事件中进行判断是否处理为一次基于时间的单击事件
基于距离的单击事件在ACTION_DOWN中没有任何处理,因为距离本身跟时间没有任何关系.

  • [x] 基于距离的单击事件
    在单点触摸中按下之后,保持任何时间(甚至无穷长),只要在ACTION_UP事件中抬起的坐标与按下坐标值距离在允许范围内即为一次基于距离的单击事件

因为存在这种特殊的单击方式,所以基于距离的单击事件只跟ACTION_DOWN/ACTION_UP有关;但这是不完善的.

存在一种可能,**在单点触摸中按下之后,触摸点进行了移动`ACTION_MOVE`事件,然后再移动回到按下的位置**
即`ACTION_DOWN`的位置坐标,此时在`ACTION_UP`事件中,触摸点的坐标没有变化,`ACTION_MOVE`中所有的操作对`ACTION_UP`是透明无效的,这可能会违背了我们需要处理的单击事件

查看单击事件的两种方式

所以需要修正这种可能存在的错误;修正方式也很简单,即然是在ACTION_MOVE中产生的问题,在ACTION_MOVE修正;
修正方式如下,针对距离单击事件

//事先定义用于标识触摸点是否产生超过单击的允许范围的移动事件(以下称为非法事件)
//**针对距离单击事件**
booelan mIsClickDistanceMove = false;
case MotionEvent.ACTION_MOVE:
    //当前移动过程中,触摸点未产生非法移动事件
    if (!mIsClickDistanceMove) {
        //进行检测
        float moveDistanceX = event.getX() - mUpX;
        float moveDistanceY = event.getY() - mUpY;
        //此处为移动允许的偏移量范围,因为手指容易抖动,增大此值可以增大容错率
        int offsetDistance = SINGLE_CLICK_OFFSET_DISTANCE;
        //触摸点移动超过单击允许范围的偏移量
        if (Math.abs(moveDistanceX) > offsetDistance
                || Math.abs(moveDistanceY) > offsetDistance) {
            //产生非法移动事件
            //一旦产生了非法移动事件,则不需要再次检测了
            mIsClickDistanceMove = true;
        }
    }

小结

以上为所有的触摸事件的处理方案

  • 主要解决的事件有:单击/双击/移动及所有触摸事件对应的回调
  • 其中单击事件分为:
    • 基于时间的单击事件
    • 基于距离的单击事件
  • 调整和修正各个事件之间的冲突关系

使用方式

直接实例化TouchEventHelper,实现其接口,重写所有抽象方法即可.
TouchEventHelper实现了View.onTouchListnener接口,通过此监听方法实现对触摸事件的操作.
在Activity中也可以使用,调用时onTouch(View,MotionEvent)传递的参数中将view设置为null即可.
整个触摸事件处理中并不会涉及任何跟view有关的操作,仅分析及处理MotionEvent

//参数为事件处理接口 onTouchEventListener
TouchEventHelper helper=new TouchEventHelper(this);
//直接在需要处理触摸事件的地方调用onTouch方法,如:
//onTouchEvent(MotionEvent event),在view中
//dispatchTouchEvent(MotionEvent event),在view,activity或者viewGroup中
helper.onTouch(null,event);
//或者直接使用view.setOnTouchListener(helper)
//activity中无法使用setOnTouchListener(),所以只能在对应的方法中进行调用

源码

源码请移步Github查看;


GitHub地址

https://github.com/CrazyTaro/TouchEventHandle

回到目录

你可能感兴趣的:(Android-触摸事件系列)