作者:陈小缘&鸿洋

起源于一位同事问我:“怎么优雅的监听双击”这个行为?
其实很多类似的事件相关的,我们都可以参考系统源码,因为有时候完全引入系统能力有些麻烦,我们可能就想顺手实现一个功能。
例如上面同事问的:
怎么优雅的监听双击?
相信大家或多或少都有一些实现方案,不过系统有给我们提供GestureDetector
类,如果你熟知该类实现,那么就能选择出于系统一样的方案,代码的认可度也会提高。
所以今天我们就借机学习下:
GestureDetector
关于支持的手势是如何检测的?
下面内容是小缘分析的。
我们在创建这个类实例的时候,需要把接口OnGestureListener
(用来监听各种手势)实现并作为参数传进它的构造方法中:

解释一下各个方法的回调时机(都非常容易理解):
-
onDown
:手指按下; -
onShowPress
:手指按下后,100毫秒内未抬起、未移动; -
onSingleTapUp
:手指按下后未移动,并在500毫秒内抬起(可以认定为单击); -
onScroll
:手指拖动; -
onLongPress
:长按(手指按下后,500毫秒内未抬起、未移动); -
onFling
:手指快速拖动后松手(惯性滚动);
除了OnGestureListener
之外,还有一个OnDoubleTapListener
,看名字就能猜到是用来监听双击事件的了:

解释一下:
-
onSingleTapConfirmed
:已经确认这是一次单击事件,想触发双击必须继续快速点击两次屏幕(即:手指抬起之后,300毫秒内没等到手指再次按下); -
onDoubleTap
:触发双击事件(手指抬起后300毫秒内再次按下(注意:是再次按下时就触发,并不是等它抬起后才触发)) -
onDoubleTapEvent
:触发双击后的手指触摸事件,包括ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
(注意:在触发长按后,不会继续收到ACTION_MOVE
事件,因为在手指长按过程中,是不需要处理手指移动的动作的,也就是会直接忽略ACTION_MOVE
的事件。还有,此方法回调后,在触发长按事件之前,如有新手指按下,则不再认定是双击了,所以不会继续回调此方法,取而代之的是onScroll
)。此方法与上面的onDoubleTap
方法的区别就是,onDoubleTap
在一次双击事件中只会回调一次,而这个方法能回调多次;
好,对它有个初步了解之后,来看看它是怎么检测这些事件的。
1,onDown
这个类的代码很少,不到800行(SDK28)。
首先来看onDown方法是在什么时候回调的 (可在刚刚的接口方法中 CTRL + Click
对应的方法名来定位到具体调用的位置) :

超级简单,监听到ACTION_DOWN
事件就立即回调了。
2,onShowPress
接着来看看onShowPress
方法(用刚刚说的方法来定位):

它会在GestureHandler
收到what
为SHOW_PRESS
的消息后回调,看看在哪里发的这个消息:

emmm,同样在收到ACTION_DOWN
后,会向mHandler
(也就是GestureHandler
)发送一个指定时间的消息,而这个时间就是事件按下的时间加上TAP_TIMEOUT
的时长,可以看到TAP_TIMEOUT
的值是根据ViewConfiguration
的getTapTimeout
方法来获取的,点开一看:

是100(ms),也就是说,当手指按下后,如果这个延时任务100毫秒内没有被取消,那么onShowPress
方法就会回调。
3,onSingleTapUp & onScroll
好,现在来看看onSingleTapUp
:

可以看到,它是在ACTION_UP
的时候回调的,回调需满足三个条件,分别是:
1.mIsDoubleTapping为false(即双击事件未触发);
2.mInLongPress为false(即长按事件未触发);
3.mAlwaysInTapRegion为true;
mAlwaysInTapRegion
什么时候为true
,什么时候为false
呢:

一共有三处赋值的地方,分别是:
1.ACTION_POINTER_DOWN
(另一只手指按下)时为false
,也就是说,如果第一只手指按下后,100毫秒内有新的手指按下,那么当手指抬起时不会触发onSingleTapUp
;
2.ACTION_DOWN
(第一只手指按下)时为true
;
3.ACTION_MOVE
时,看else if
里面的那个if
, 它是判断distance
(手指的移动距离)是否大于slopSquare
(触发移动的最小距离),如果是的话,会回调onScroll
方法,并把mAlwaysInTapRegion
设为false
,这就说明,如果手指按下100秒内开始了拖动的话,那么onSingleTapUp
方法也是不会回调的;
还可以看到当mAlwaysInTapRegion
被设为false
之后,下一次的ACTION_MOVE
到来时,如果没有触发双击(即上面的mIsDoubleTapping为false
)并且手指的水平或垂直移动距离不为0的话,就会一直回调onScroll
方法。
好,现在onScroll
也讲了,轮到onLongPress
了。
4,onLongPress

跟onShowPress
方法一样也是借助Handler
的定时消息机制来实现的,它在收到LONG_PRESS
的消息之后,会调用dispatchLongPress
方法,dispatchLongPress
方法首先会标记mInLongPress
为true
(注意:这将会影响到上面说到的onSingleTapUp
的回调,因为onSingleTapUp
的回调条件是需要mInLongPress
为false
的(即未触发长按事件))
接着就回调onLongPress
方法。
那么LONG_PRESS
消息在什么时候发送的呢?
也是在ACTION_DOWN
的时候 :

在发送消息之前,会先检查是否开启了监听长按事件,还有取消上一次发出且未执行的长按回调任务。
可以看到定的时间为事件按下时间加上getLongPressTimeout
方法返回的时长,默认是500(ms),也就是当手指按下半秒后,onLongPress
方法就会被回调,当然了,前提是这个任务没有被取消。
有以下几种情况会导致长按回调任务被取消:
500ms内有新手指按下;
500ms内触发了onScroll
,即手指移动超过指定距离;
500ms内手指抬起;
500ms内收到了ACTION_CANCEL
事件(该ACTION
一般源自父容器的私自创建);
来看看onFling
:
5,onFling

跟我们平时处理惯性滚动没什么区别,只是它在回调之前会先判断滑动的速度 是否大于 指定的最小速度,否则不进行滚行滚动。
好,最后我们来看一下onSingleTapConfirmed
、onDoubleTap
、onDoubleTapEvent
分别是怎么处理的:
onSingleTapConfirmed、onDoubleTap、onDoubleTapEvent
private class GestureHandler extends Handler {
......
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
......
case TAP:
if (mDoubleTapListener != null) {
if (!mStillDown) {
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
} else {
mDeferConfirmSingleTap = true;
}
}
break;
......
}
}
}
public boolean onTouchEvent(MotionEvent ev) {
......
boolean handled = false;
switch (action & MotionEvent.ACTION_MASK) {
......
case MotionEvent.ACTION_DOWN:
......
if (isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
mIsDoubleTapping = true;
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else {
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
mStillDown = true;
......
break;
case MotionEvent.ACTION_MOVE:
if (mIsDoubleTapping) {
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
......
break;
case MotionEvent.ACTION_UP:
mStillDown = false;
if (mIsDoubleTapping) {
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
......
if (mIsDoubleTapping) {
......
} else if (mInLongPress) {
......
} else if (mAlwaysInTapRegion) {
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
}
......
break;
......
}
......
return handled;
}
首先看onSingleTapConfirmed
方法,它在两个地方有调用,分别是:
GestureHandler
收到TAP
消息时
处理ACTION_UP
事件时
当GestureHandler
收到TAP
消息时它会先检查手指是否已经抬起(!mStillDown
),如果已经抬起了的话,就会立即调用,否则把mDeferConfirmSingleTap
标记为true
,表示onSingleTapConfirmed
方法应在ACTION_UP
时回调,可以看到在处理ACTION_UP
时,如果手指没有移动过并且没触发长按的话,就会判断mDeferConfirmSingleTap
是否为true
,是的话,就会回调onSingleTapConfirmed
方法。
接着看ACTION_DOWN
,它会调用isConsideredDoubleTap
方法来判断此次事件是否被认定是双击,如果不是,就会向GestureHandler
发一条延时消息(延时回调onSingleTapConfirmed
方法)
如果是的话,就会把mIsDoubleTapping
标记为true
,然后依次回调onDoubleTap
和onDoubleTapEvent
。可以看到在ACTION_MOVE
和ACTION_UP
中也会根据mIsDoubleTapping
来判断是否继续回调onDoubleTapEvent
方法。
好,那现在来看一下,它究竟是怎么认定为双击的,看看isConsideredDoubleTap
方法:

先是判断了mAlwaysInBiggerTapRegion
,如果它为false
的话,则代表被其他动作(ACTION_MOVE
、ACTION_CANCEL
)中断了双击事件的检测,所以直接返回false
(即不认定是双击)了。
接着会判断第二次按下与第一次按下的时间间隔,如果大于300毫秒则认定是超时,如果小于40毫秒也会忽略(太快了)。
最后,可能很多同学咋一看,看不出来是什么逻辑,仔细看几次,就会知道这其实是在计算第一次按下和第二次按下的坐标间隔距离,用的就是计算两点间距离的公式(√(x1 - x2)² + (y1 - y2)²)。
这时有同学可能会问:
不是还要开平方吗?怎么它代码里没有呢?
其实,那个slopSquare
(也就是能够被认定为双击的最大间隔)在初始化时,就已经作了平方运算了,所以这里就不需要开平方了。
emmm,那么isConsideredDoubleTap
方法最后一句的意思就是,判断两次触摸事件的坐标间隔是否在指定的最大间隔范围内,如果是的话,则认定是双击。
最后附上我的Android核心技术学习大纲,获取相关内容来我的GitHub一起玩耍:https://github.com/Meng997998/AndroidJX
你把你的时间投资在学习上,就意味着你可以收获技能,更有机会增加收入。
在这里分享我的 Android学习PDF大全来学习,这份Android学习PDF大全真的包含了方方面面了,内含Java基础知识点、Android基础、Android进阶延伸、算法合集等等
我的这份学习合集,可以有效的帮助大家掌握知识点。
总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
Android相关学习内容:关注我看个人介绍,或者直接简信我
网友评论