Android触摸事件(一)-TouchEventHelper

文件夹

概述

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

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

此类能够处理的事件包含:

  • 单击事件(基于时间与距离的两种单击事件,详见下文)
  • 双击事件
  • 单点触摸移动事件(可用于实现界面拖动)
  • 多点触摸移动事件(可用于实现界面缩放)
  • 全部触摸事件的相应回调(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

因此须要设置不同标识在不同方式的单击事件中使用;
当然,如今已经加入了两种检測方式仅仅能触发一种的处理,所以这个问题easy处理非常多.

//是否触发基于时间的单击事件
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`是透明无效的,这可能会违背了我们须要处理的单击事件

查看单击事件的两种方式

所以须要修正这样的可能存在的错误;修正方式也非常easy,即然是在ACTION_MOVE中产生的问题,在ACTION_MOVE修正;
修正方式例如以下,针对距离单击事件

//事先定义用于标识触摸点是否产生超过单击的同意范围的移动事件(下面称为非法事件)
//**针对距离单击事件**
booelan mIsClickDistanceMove = false;
case MotionEvent.ACTION_MOVE:
    //当前移动过程中,触摸点未产生非法移动事件
    if (!mIsClickDistanceMove) {
        //进行检測
        float moveDistanceX = event.getX() - mUpX;
        float moveDistanceY = event.getY() - mUpY;
        //此处为移动同意的偏移量范围,由于手指easy抖动,增大此值能够增大容错率
        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

回到文件夹

posted @ 2017-08-20 18:10  jhcelue  阅读(1146)  评论(0编辑  收藏  举报