目录
- 目录
- 概述
- 关于单点触摸事件singleTouch
- 单击的两种方式
- 关于双击事件
- 双击事件的检测逻辑
- 双击事件触发的时机
- 关于多点触摸事件multiTouch
- 两点触摸中的移动事件
- 两点触摸事件的触发过程
- 两点触摸的事件
- 实现
- 变量定义
- 触摸事件流程
- 自定义事件计时方案
- 自定义事件触发区域
- 触摸事件处理规则
- 关于回调的方法
- 触摸事件处理源码
- 双击事件的优化处理
- 如何检测当次触摸事件的单击事件
- 如何检测触发双击事件
- 辅助补充逻辑
- 小结
- 使用方式
- 源码
概述
这是一个 触摸 事件统一处理辅助类;处理的主要是点击事件,其中包括了:
- 单点触摸事件
- 多点(两点)触摸事件
此类可以处理的事件包括:
- 单击事件(基于时间与距离的两种单击事件,详见下文)
- 双击事件
- 单点触摸移动事件(可用于实现界面拖动)
- 多点触摸移动事件(可用于实现界面缩放)
- 所有触摸事件的相应回调(
down
/move
/up
事件)
关于单点触摸事件(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<250){ //处理单击事件 } //否则不视为一次单击事件 break;}
通过计算按下时与抬起时的时间差来确定是否是一次单击事件(250ms足够了),这是基于时间的单击事件;
基于距离的单点触摸事件(
singleTouchByDistance
)
从上面我们知道单点触摸时也是可能触发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_Xswitch(event.getAction() & MotionEvent.ACTION_MASK){ case MotionEvent.ACTION_X: //单点触摸事件处理 break; case MotionEvent.ACTION_POINTER_X: //多点触摸事件处理 brea;}
两点触摸中的移动事件
首先,必须注意的一个点是:
多点触摸事件中移动时触发的移动事件也是
ACTION_MOVE
也就是说ACTION_MOVE
事件是移动的通用事件,在单点触摸移动和多点触摸移动中都存在.
两点触摸事件的触发过程
除以上提及的共用ACTION_MOVE
事件之外,多点触摸事件可能存在的过程是这样的:
ACTION_DOWN
->ACTION_POINTER_DOWN
->ACTION_MOVE
->ACTION_POINTER_UP
->ACTION_UP
- 这种情况是在两点触摸时,两个手指刚好 同时按上 -> 移动 -> 同时抬起
很明显,既然存在同时触摸,也肯定存在非同时触摸了.当非同时触摸时的过程是这样的:
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_DOWN
与ACTION_POINTER_UP
),之后单点触摸抬起,触发ACTION_UP
;
- 至于第一个
ACTION_MOVE
事件是否会触发取决于第一个触摸点与第二个触摸点之间的时间间距(如果第二次按下的时间与第一次按下时间间隔足够短,则不会触发); - 同理第二个
ACTION_MOVE
取决于多点触摸的按下与抬起的时间差,类似于单击,多点触摸按住时,ACTION_MOVE
依然是正常触发,但距离值还是0
. - 而第三个
ACTION_MOVE
是所有多点触摸抬起后(只剩下单点触摸时),若还保持单点触摸(不管有没有移动)就会触发第三轮的ACTION_MOVE
事件
以上过程可以明确得到:
不管是
同时多点触摸
还是间接多点触摸
,ACTION_DOWN
与ACTION_UP
两个事件是必定会触发并永远在第一项和最后一项事件
所以,在处理多点触摸的事件时,必须小心处理ACTION_MOVE
与ACTION_UP
事件,因为这两个事件并不是单点触摸
专属的事件,而是所有的触摸事件都会触发的
两点触摸的事件
在两点触摸时,一般我们不考虑两点"单击"
事件,主要是针对两点触摸时触发的移动事件进行处理;往往这种情况需要处理的是类似放大/缩小的功能
如何判断两点触摸事件并处理
对于两点触摸事件,这个很好判断;当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_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
比较特殊,存在两个回调可能
- 单点触摸移动事件回调:
onSingleTouchEventHandle(MotionEvent,int)
- 多点触摸移动事件回调:
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
事件
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;
此处的实际应用场景在于:当界面被拖动移动时(依赖于
MOVE
事件),切换到多点触摸状态时可以保证界面的正常(依赖于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)
- 当第一种方式单击成立之后,本次触摸事件单击事件成功,标识置为true;
- 当第二种方式检测时,最后一次单击标识为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;}//若处理了时间单击事件,对应标识置为trueisFireTimeClickEvent=true;//同上检测双击事件//若处理了距离单击事件,对应标识置为trueisFireDistanceClickEvent=true;//保存此次单击状态isFireLastClickEvent = isFireTimeClickEvent || isFireDistanceClickEvent;
辅助补充逻辑
在以上的触摸处理事件中,我们提到单击分为两中方式:
- 基于时间的单击事件
- 基本距离的单击事件
基于时间的单击事件处理已经在ACTION_DOWN
事件中操作了,通过延时发送取消按下标识,再从ACTION_UP
事件中进行判断是否处理为一次基于时间的单击事件
基于距离的单击事件在ACTION_DOWN
中没有任何处理,因为距离本身跟时间没有任何关系.
在单点触摸中按下之后,保持任何时间(甚至无穷长),只要在
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 + 20; //触摸点移动超过单击允许范围的偏移量 if (Math.abs(moveDistanceX) > offsetDistance || Math.abs(moveDistanceY) > offsetDistance) { //产生非法移动事件 //一旦产生了非法移动事件,则不需要再次检测了 mIsClickDistanceMove = true; } }
小结
以上为所有的触摸事件的处理方案
- 主要解决的事件有:单击/双击/移动及所有触摸事件对应的回调
- 其中单击事件分为:
- 基于时间的单击事件
- 基于距离的单击事件
- 调整和修正各个事件之间的冲突关系
使用方式
直接继承此类,重写所有抽象方法即可.
源码
/** * Created by CT in 2015-08-14 * <p>抽像类,处理触摸事件,区分单击及多点触摸事件</p> * <p>此类中使用到handler,请确保使用在UI线程或者是自定义looper的线程中(一般也没有人会把触摸事件放在非UI线程吧 =_=)</p> */public abstract class AbsTouchEventHandle implements View.OnTouchListener { /** * 距离双击事件 */ public static final int EVENT_DOUBLE_CLICK_BY_TIME = 0; public static final int EVENT_SINGLE_CLICK_BY_TIME = -1; public static final int EVENT_SINGLE_CLICK_BY_DISTANCE = -2; /** * 额外分配的触摸事件,用于建议优先处理的触摸事件 */ public static final int MOTION_EVENT_NOTHING = 0; /** * 处理时间单击事件 */ private static final int CONSUME_LAST_SINGLE_CLICK_EVENT = -1; /** * 处理单点触摸下的事件 */ private static final int HANDLE_SINGLE_DOWN = 2; private static String TAG = "touch_event"; //已经触发单击事件的情况下,是否触发单点触摸事件 private boolean mIsTriggerSingleTouchEvent = true; private boolean mIsShowLog = false; private int mMultiTouchCount = 0; //是否开始触发本次时间单击事件(整个触摸事件) private boolean mIsFireTimeClickEvent = false; //是否开始触发本次距离单击事件 private boolean mIsFireDistanceClickEvent = false; //是否完成上一次单击(一次) private boolean mIsFireLastClickEvent = false; //多点触摸按下 private boolean mIsMultiDown = false; //是否单点触摸按下 private boolean mIsSingleDown = false; //是否进入单击移动事件 private boolean mIsSingleMove = false; //是否进入移动事件 private boolean mIsInMotionMove = false; //单次单击事件中(针对距离单击),触摸点是否产生超过单击的允许范围的移动事件 private boolean mIsClickDistanceMove = false; //时间单击事件是否可用 private boolean mIsClickTimeEventEnable = true; //距离单击事件是否可用 private boolean mIsClickDistanceEventEnable = true; //单击事件的可持续最长时间间隔(down与up事件之间的间隔) private int SINGLE_CLICK_INTERVAL = 250; //双击事件可可持续最长时间间隔(两次单击事件之间的间隔) private int DOUBLE_CLICK_INTERVAL = 350; //距离单击事件最大的允许偏移量大小 private int SINGLE_CLICK_OFFSET_DISTANCE = 10; private float mDownX = 0f; private float mDownY = 0f; private float mUpX = 0f; private float mUpY = 0f; private Handler mHandle = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { //取消完成一次单击事件的标识(用于识别双击事件) case CONSUME_LAST_SINGLE_CLICK_EVENT: mIsFireLastClickEvent = false; break; //取消单点触摸的有效时间(用于识别基于时间的单击事件) case HANDLE_SINGLE_DOWN: mIsSingleDown = false; break; } } }; /** * 检测单击事件的触发,触发单击事件返回 true,否则返回 false * * @param event 触摸事件 * @param fireEvent 需要检测的事件,{@link #EVENT_SINGLE_CLICK_BY_TIME}基于时间的单击事件; * {@link #EVENT_SINGLE_CLICK_BY_DISTANCE}基于距离的单击事件 * @return */ private boolean isFireSingleClickEvent(MotionEvent event, int fireEvent) { //不触发多点触摸事件的情况下才进行单击事件的判断处理 if (!mIsMultiDown) { if (fireEvent == EVENT_SINGLE_CLICK_BY_TIME) { //根据时间处理单击事件 //触摸点down之后500ms内触摸点抬起则认为是一次单击事件 //两次单击事件之间的时间间隔在允许间隔内即为一次双击事件 //是否在单点触摸按下时间间隔内(该变量在按下后指定间隔时间内重置,此处是基于时间的单击事件) if (mIsSingleDown) { //触发单击事件 return true; } } else if (fireEvent == EVENT_SINGLE_CLICK_BY_DISTANCE) { //移动距离单击处理事件 //触摸点down事件的坐标与up事件的坐标距离不超过10像素时,认为一次单击事件(与时间无关) //两次单击事件之间的时间间隔在500ms内则认为是一次双击事件 mUpX = event.getX(); mUpY = event.getY(); float moveDistanceX = mUpX - mDownX; float moveDistanceY = mUpY - mDownY; //根据触摸点up与down事件的坐标差判断是否为单击事件(不由时间决定) if (Math.abs(moveDistanceX) < SINGLE_CLICK_OFFSET_DISTANCE && Math.abs(moveDistanceY) < SINGLE_CLICK_OFFSET_DISTANCE) { //触摸单击事件 return true; } } } return false; } /** * 处理一次单击事件,此过程不负责单击事件的检测,只负责执行 * * @param event * @param handleEvent */ private void handleSingleClickEvent(MotionEvent event, int handleEvent) { if (handleEvent == EVENT_SINGLE_CLICK_BY_TIME) { //完成一次时间单击事件 showMsg("单击事件 single"); this.onSingleClickByTime(event); //记录本次触摸了单击事件 mIsFireTimeClickEvent = true; //处理事件为距离单击事件且在移动过程中不可超过允许范围 } else if (handleEvent == EVENT_SINGLE_CLICK_BY_DISTANCE && !mIsClickDistanceMove) { //完成一次距离单击事件 showMsg("单击事件(距离) single"); this.onSingleClickByDistance(event); //记录本次触摸了单击事件 mIsFireDistanceClickEvent = true; } } /** * 处理一次点击事件,完整的一个过程,包括检测/执行/反馈;处理事件包括单击事件/双击事件</br> * 若触发双击事件,返回true,其它情况返回false * * @param isDistanceEnable 基于距离的单击事件是否可用 * @param isTimeEnable 基于时间的单击事件是否可用 * @param fireEvent 需要触发的事件,一次只能处理一个事件;{@link #EVENT_SINGLE_CLICK_BY_TIME}基于时间的单击事件 * {@link #EVENT_SINGLE_CLICK_BY_DISTANCE}基于距离的单击事件 * @param event 触摸事件 * @return 若触发双击事件, 返回true, 其它情况返回false */ private boolean handleClickEvent(boolean isDistanceEnable, boolean isTimeEnable, int fireEvent, MotionEvent event) { //检测指定事件是否触发 if (this.isFireSingleClickEvent(event, fireEvent)) { //若事件触发,检测是否已经执行过一次单击事件且在有效时间间隔内 //并且必须不在是本次触摸事件中触发的单击事件 //mIsFireTimeClickEvent与mIsFireDistanceCLickEvent用于检测本次是否触发单击事件 //mIsFireLastClickEvent用于检测上一次单击事件是否在有效时间范围内 if ((mIsFireTimeClickEvent || mIsFireDistanceClickEvent) && mIsFireLastClickEvent) { //触摸双击事件 showMsg("双击事件(时间) double"); this.onDoubleClickByTime(); //取消双击事件的标识 this.cancelDoubleClickEvent(EVENT_DOUBLE_CLICK_BY_TIME); //返回已触发双击事件 return true; } else { //未执行过一次单击事件 //触发对应的单击事件 if (fireEvent == EVENT_SINGLE_CLICK_BY_TIME && isTimeEnable) { this.handleSingleClickEvent(event, fireEvent); } else if (fireEvent == EVENT_SINGLE_CLICK_BY_DISTANCE && isDistanceEnable) { this.handleSingleClickEvent(event, fireEvent); } } } return false; } /** * 结束触摸事件,重置所有应该重置的变量 */ private void finishTouchEvent() { //取消移动状态的记录 mIsInMotionMove = false; //多点单击的标志必须在此处才可以被重置 //因为多点单击的抬起事件优先处理于单击的抬起事件 //如果在多点单击的抬起事件时重置该变量则会导致上面的判断百分百是成立的 mIsMultiDown = false; mIsSingleDown = false; mIsSingleMove = false; mIsFireTimeClickEvent = false; mIsClickDistanceMove = false; mMultiTouchCount = 0; //记录此触摸事件中是否产生了单击事件(用于后续的双击事件判断) mIsFireLastClickEvent = mIsFireTimeClickEvent || mIsFireDistanceClickEvent; //重置本次单击事件的标识 mIsFireTimeClickEvent = false; mIsFireDistanceClickEvent = false; //发送延迟消费记录的单击事件标识 mHandle.sendEmptyMessageDelayed(CONSUME_LAST_SINGLE_CLICK_EVENT, DOUBLE_CLICK_INTERVAL); //mIsFireLastClickEvent 此变量不可以重置,这是保存已经完成一次单击事件的标识,用于后续识别双击事件 } @Override public boolean onTouch(View v, MotionEvent event) { 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; mMultiTouchCount += 1; this.onMultiTouchEventHandle(event, MOTION_EVENT_NOTHING); break; case MotionEvent.ACTION_UP: showMsg("单点触摸 up"); //任何一种事件中,只要触发了双击事件,则结束事件 //优先检测基于距离的单击事件 if (this.handleClickEvent(mIsClickDistanceEventEnable, mIsClickTimeEventEnable, EVENT_SINGLE_CLICK_BY_DISTANCE, event)) { this.finishTouchEvent(); break; } //检测基于时间的单击事件 if (this.handleClickEvent(mIsClickDistanceEventEnable, mIsClickTimeEventEnable, EVENT_SINGLE_CLICK_BY_TIME, event)) { this.finishTouchEvent(); break; } //允许触发单点触摸事件 if (mIsTriggerSingleTouchEvent) { //在处理单击事件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(event, MOTION_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); } mMultiTouchCount -= 1; //此处不重置mIsMultiDown变量是因为后面检测单击事件的up与多点触控的up需要 //而且此处不重置并不会对其它的部分造成影响 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); } //当可能发生一次距离单击事件时,需要检测是否产生了超过偏移量的移动距离 //此处不在up事件中判断是因为: //存在一种可能是(由于距离单击事件有足够长的时间),在move的时候移动距离超过偏移量 //但之后又移动到单击位置的坐标,在up事件中move所引起的坐标变化是不可见的 //所以于对up事件,down的坐标与up的坐标偏移量是允许范围内,会处理为一次距离单击事件 //但实际上这是一次移动事件 if (!mIsClickDistanceMove) { float moveDistanceX = event.getX() - mUpX; float moveDistanceY = event.getY() - mUpY; int offsetDistance = SINGLE_CLICK_OFFSET_DISTANCE + 20; if (Math.abs(moveDistanceX) > offsetDistance || Math.abs(moveDistanceY) > offsetDistance) { //一旦取消了距离单击事件有效性的标识,则不需要再次检测了 mIsClickDistanceMove = true; } } break; } return true; } /** * 设置单击有效的时间间隔(down与up事件的最长允许时间间隔),默认250ms * * @param interval */ public void setSingleClickInterval(int interval) { this.SINGLE_CLICK_INTERVAL = interval; } /** * 设置双击有效时间间隔(两次单击事件的最长允许时间间隔),默认350ms * * @param interval */ public void setDoubleClickInterval(int interval) { this.DOUBLE_CLICK_INTERVAL = interval; } /** * 设置单击允许的最大偏移量范围(坐标偏移像素值),默认10像素 * * @param offsetDistance */ public void setSingleClickOffsetDistance(int offsetDistance) { this.SINGLE_CLICK_OFFSET_DISTANCE = offsetDistance; } /** * 基于时间的单击事件是否可响应,若为true则事件触发时回调响应;若为false事件触发时不回调 * * @param isEnabled */ public void setIsEnableSingleClickByTime(boolean isEnabled) { this.mIsClickTimeEventEnable = isEnabled; } /** * 基于距离的单击事件是否可响应,若为true则事件触发时回调响应;若为false事件触发时不回调 * * @param isEnabled */ public void setIsEnableSingleClickByDistance(boolean isEnabled) { this.mIsClickDistanceEventEnable = isEnabled; } /** * 设置在触发单击事件时是否同时触发单点触摸事件;默认触发 * <p>单击事件本身属于单点触摸事件之中的一种,只是触摸时间在500ms以内则认为是单击事件,但同时是满足触发单点触摸事件的(此处仅指up事件)</p> * 在up事件中,事件优先处理级如下: 双击 > 单击 > 普通的UP事件 * * @param isTrigger true为同时触发,false为忽略单点触摸事件 */ public void setIsTriggerSingleTouchEvent(boolean isTrigger) { this.mIsTriggerSingleTouchEvent = isTrigger; } /** * 设置是否显示log * * @param isShowLog * @param tag tag为显示log的标志,可为null,tag为null时使用默认标志"touch_event" */ public void setIsShowLog(boolean isShowLog, String tag) { if (tag != null) { TAG = tag; } else { TAG = "touch_event"; } this.mIsShowLog = isShowLog; } /** * 打印默认的log,默认标志为:touch_event * * @param msg 打印消息 */ public void showMsg(String msg) { if (mIsShowLog) { Log.i(TAG, msg); } } /** * 打印log * * @param tag 标志tag * @param msg 打印信息 */ public void showMsg(String tag, String msg) { if (mIsShowLog) { Log.i(tag, msg); } } /** * <font color="#ff9900">取消事件有效性,目前仅对双击事件有效{@link #onDoubleClickByTime()}</font><br/> * 每一次单击事件之后会有一个暂存的延迟标识,在允许时间内再次触发单击事件时,此时不会响应单击事件,而是转化成双击事件 * * @param event 需要取消的事件 */ public void cancelDoubleClickEvent(int event) { switch (event) { case EVENT_DOUBLE_CLICK_BY_TIME: //一旦取消双击事件,所有有关的变量都重置 this.mIsFireLastClickEvent = false; this.mIsFireTimeClickEvent = false; this.mIsFireDistanceClickEvent = false; break; } } /** * 单点触摸事件处理 * * @param event 单点触摸事件 * @param extraMotionEvent 建议处理的额外事件,如果不需要进行额外处理则该参数值为{@link #MOTION_EVENT_NOTHING} * <p>存在此参数是因为可能用户进行单点触摸并移动之后,会再进行多点触摸(此时并没有松开触摸),在这种情况下是无法分辨需要处理的是单点触摸事件还是多点触摸事件. * <font color="#ff9900"><b>此时会传递此参数值为单点触摸的{@link MotionEvent#ACTION_UP},建议按抬起事件处理并结束事件</b></font></p> */ public abstract void onSingleTouchEventHandle(MotionEvent event, int extraMotionEvent); /** * 多点触摸事件处理(两点触摸,暂没有做其它任何多点触摸) * * @param event 多点触摸事件 * @param extraMotionEvent 建议处理的额外事件,如果不需要进行额外处理则该参数值为{@link #MOTION_EVENT_NOTHING} */ public abstract void onMultiTouchEventHandle(MotionEvent event, int extraMotionEvent); /** * 单击事件处理,由于只要触摸到屏幕且时间足够长,就可以产生move事件,并不一定需要移动触摸才能产生move事件, * <font color="#ff9900"><b>所以产生单击事件的同时也会触发up事件{@link #onSingleTouchEventHandle(MotionEvent, int)}</b></font>, * <p>单击事件仅仅只能控制触摸时间<font color="#ff9900"><b>少于500ms</b></font>的触摸事件,超过500ms将不会触摸单击事件</p> * * @param event 单击触摸事件 */ public abstract void onSingleClickByTime(MotionEvent event); /** * 单击事件处理,触摸点down的坐标与up坐标距离差不大于10像素则认为是一次单击,<font color="#ff9900"><b>与时间无关</b></font> * * @param event 单击触摸事件 */ public abstract void onSingleClickByDistance(MotionEvent event); /** * 双击事件处理,每次单击判断由时间决定,参考{@link #onSingleClickByTime(MotionEvent)} */ public abstract void onDoubleClickByTime();}
回到目录