前言
安卓源码给我们提供了一个GestureDetector
类,来监听手势,点击,长按,双击,滚动,抛等。本文通过解读Gesture类来看看安卓源码是怎样判断各类手势的?搞清楚了这个,我们就可以自定义手势啦,比如一张图片,点击返回,长按保存,单个手指双击 图片放大到2倍或者由放大状态变回原来的大小,两个手指捏 缩小图片,两个手指张开 放大图片等等。
下面我们就来看GestureDetector
类的具体实现。涉及到fling抛的代码会放在后面讲,本文会略过。
1. GestureDetector类中的接口
GestureDetector
类提供了两个接口OnGestureListener
和OnDoubleTapListener
,OnGestureListener
用来监听单个手指事件,OnDoubleTapListener
用来监听两个手指事件。具体方法如下:
接口OnGestureListener
:
public interface OnGestureListener {
//手指按下就会触发(调用onTouch()方法的ACTION_DOWN事件时触发)
boolean onDown(MotionEvent e);
//一次点击up事件;在touch down后又没有滑动(onScroll),又没有长按(onLongPress),然后Touchup时触发
boolean onSingleTapUp(MotionEvent e);
/*
down事件发生而move或则up还没发生前触发该
事件;Touch了还没有滑动时触发(与onDown,onLongPress)比较onDown只要Touch down一定立刻触发。而Touchdown后过一会没有滑动先触发onShowPress再是onLongPress。所以Touchdown后一直不滑动
onLongPress之前触发
*/
void onShowPress(MotionEvent e);
//长按,触摸屏按下后既不抬起也不移动,过一段时间后触发
void onLongPress(MotionEvent e);
//滚动,触摸屏按下后移动(执行onTouch()方法的ACTION_MOVE事件时会触发)
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
//滑动,触摸屏按下后快速移动并抬起,会先触发滚动手势,跟着触发一个滑动手势
//在ACTION_UP时才会触发
/*参数:
e1:第1个ACTION_DOWN MotionEvent 并且只有一个;
e2:最后一个ACTION_MOVE MotionEvent ;
velocityX:X轴上的移动速度,像素/秒 ;
velocityY:Y轴上的移动速度,像素/秒.
触发条件:X轴的坐标位移大于FLING_MIN_DISTANCE,且移动速度大于FLING_MIN_VELOCITY个像素/秒
*/
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
接口OnDoubleTapListener
:
public interface OnDoubleTapListener {
/*
1. 单击确认,用来判定该次点击是SingleTap而不是DoubleTap,如果连续点击两次就是DoubleTap手势,如果只点击一次,系统等待一段时间后没有收到第二次点击则判定该次点击为SingleTap而不是DoubleTap,然后触发SingleTapConfirmed事件。
2. 不同于onSingleTapUp,他是在GestureDetector确信用户在第一次触摸屏幕后,没有紧跟着第二次触摸屏幕,也就是不是“双击”的时候触发
*/
boolean onSingleTapConfirmed(MotionEvent e);
//在双击的第二下,Touch down时触发 。
boolean onDoubleTap(MotionEvent e);
//双击的第二下Touch down和up都会触发,可用e.getAction()区分。
boolean onDoubleTapEvent(MotionEvent e);
}
各种手势调用方法如下:
- 快速点击(没有滑动):onDown() -> onSingleTapUp() -> onSingleTapConfirmed()
- 手指一直按下,即长按(没有滑动,没有抬起):onDown() -> onShowPress() -> onLongPress()
- 滚动:onDown() -> onScroll() -> onScroll() ... ...
- 抛:onDown() -> onScroll() -> ... ... -> onFling()
(onTouch()::ACTION_DOWN) -> onDown() -> (onTouch()::ACTION_MOVE) -> onScroll() -> ... ...(onTouch()::ACTION_MOVE) ->onScroll() -> (onTouch()::ACTION_UP) -> onFling()
2. GestureDetector类中的内部类SimpleOnGestureListener
内部类SimpleOnGestureListener
,实现了上面2个接口:
public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
public void onLongPress(MotionEvent e) {
}
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
return false;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
return false;
}
public void onShowPress(MotionEvent e) {
}
public boolean onDown(MotionEvent e) {
return false;
}
public boolean onDoubleTap(MotionEvent e) {
return false;
}
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
public boolean onSingleTapConfirmed(MotionEvent e) {
return false;
}
}
3. 消息类GestureHandler
GestureDetector
类中的消息类型有3种:TAP,SHOW_PRESS,LONG_PRESS,
-
SHOW_PRESS:回调onShowPress()接口
-
LONG_PRESS:回调onLongPress()接口,即长按事件。
-
TAP:如果当前手指不是down状态,回调onSingleTapConfirmed()接口,即单击事件。
具体实现如下:
private class GestureHandler extends Handler {
GestureHandler() {
super();
}
GestureHandler(Handler handler) {
super(handler.getLooper());
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PRESS:
mListener.onShowPress(mCurrentDownEvent);
break;
case LONG_PRESS:
dispatchLongPress();
break;
case TAP:
// If the user's finger is still down, do not count it as a tap
if (mDoubleTapListener != null) {
if (!mStillDown) {
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
} else {
mDeferConfirmSingleTap = true;
}
}
break;
default:
throw new RuntimeException("Unknown message " + msg); //never
}
}
}
//处理长按事件
private void dispatchLongPress() {
mHandler.removeMessages(TAP);
mDeferConfirmSingleTap = false;
mInLongPress = true;
mListener.onLongPress(mCurrentDownEvent);
}
4. 单击,长按,双击,移动等这些事件的判断条件
源码是怎么判断单击,长按,双击,移动等这些事件的呢?当手指处于按下状态,即down事件,会发送一些消息TAP,LONG_PRESS,SHOW_PRESS(延时发送或在指定的时间点发送),然后在接下来的事件中分别去判断,如果是移动事件,移除TAP,LONG_PRESS和SHOW_PRESS消息;如果是长按事件,就移除TAP消息。这样,如果没有移除TAP消息,那么handler中会回调相应接口。一般双击的判断在down事件,移动的判断发生在move事件,而双击的消费发生在up事件,回调onShowPress()
,长按回调onLongPress()
,单击回调onSingleTapConfirmed()
都发生在handleMessage
中。
4.1 单击
点击时间在 DOUBLE_TAP_TIMEOUT 内未进行第二次单击事件。
单击的回调接口为onSingleTapConfirmed()
。onSingleTapConfirmed()
的调用有两个地方:
- handlerMessage中
- up事件中
handlerMessage
中消息类型为TAP并且只要当前不是按下状态就会调用,up事件中,首先判断双击和长按是否正在进行,如果都不是,判断如果移动区域在mTouchSlopSquare之内并且mDeferConfirmSingleTap
为true,会调用。
4.2 长按
- 手指处于按下状态
- 按下时间未超过LONGPRESS_TIMEOUT。通过
ViewConfiguration.getLongPressTimeout()
来获取- 移动未超过mTouchSlopSquare的距离。
4.3 双击
是否有上一次的单击事件(上一次down事件和up事件不为空)
mAlwaysInBiggerTapRegion为true。即每次单击的移动距离在 mDoubleTapTouchSlopSquare 之内。
第二次单击的down事件和第一次单击的up事件的时间间隔在 DOUBLE_TAP_MIN_TIME 和 DOUBLE_TAP_TIMEOUT之间。
第一次按下的位置和第二次按下的位置之间的距离小于mDoubleTapSlopSquare。
源码如下:
boolean hadTapMessage = mHandler.hasMessages(TAP);
if (hadTapMessage) mHandler.removeMessages(TAP);
if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
// This is a second tap
mIsDoubleTapping = true;
}
private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
MotionEvent secondDown) {
if (!mAlwaysInBiggerTapRegion) {
return false;
}
final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
return false;
}
int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
}
4.4 移动
mAlwaysInTapRegion为false 或者 mAlwaysInTapRegion为true并且 移动区域在 mTouchSlopSquare 和 mDoubleTapTouchSlopSquare 之间。
5. 各个事件的具体实现和分析
看源码之前,我们先来看一些变量。
private boolean mStillDown;//是否按下。ACTION_DOWN为true,ACTION_UP 和 cancle 为false
//是否延迟确定点击。确定点击即消费点击,其实就是点击事件。在handleMessage中消息类型为TAP时,如果手指为非按下状态,则回调onSingleTapConfirmed接口,否则,mDeferConfirmSingleTap置为true,其他情况全部置为false:ACTION_DOWN,ACTION_UP结尾,处理长按事件,cancle。
private boolean mDeferConfirmSingleTap;
private boolean mInLongPress;//是否正在处理长按事件
//是否允许双击事件发生,默认为true,除非手动去设置。只有为true,双击事件才能进行
private boolean mIsLongpressEnabled;
private boolean mIsDoubleTapping;//当前事件是否是双击事件
//是否当前手指仅在小范围内移动,当手指仅在小范围内移动时,视为手指未曾移动过,不会触发onScroll手势。用来判断点击事件和移动事件。移动距离大于mTouchSlopSquare时,为false。
private boolean mAlwaysInTapRegion;
//是否当前手指在较大范围内移动,判断双击手势。此值为true时,双击手势成立。移动距离大于mDoubleTapTouchSlopSquare时,为false。
private boolean mAlwaysInBiggerTapRegion;
关于点击区域的变量:
// 判断是否是单击,是否能构成onScroll事件。移动距离小于mTouchSlopSquare,则是单击,大于,则是onScroll
private int mTouchSlopSquare;
//判断是否构成双击,单次单击移动距离小于mDoubleTapTouchSlopSquare,mAlwaysInBiggerTapRegion为true。
private int mDoubleTapTouchSlopSquare;
//判断是否构成双击,第二次按下的位置和第一次抬起的位置的距离小于mDoubleTapSlopSquare,则构成双击
private int mDoubleTapSlopSquare;
关于时间的变量:
//发送SHOW_PRESS消息的时间,发生在ACTION_MOVE和ACTION_UP之前。如果在ACTION_MOVE中判断为onScroll事件,则 //取消SHOW_PRESS消息。
private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
//判断双击
private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
- 发送长按LONG_PRESS消息的延时为:
mCurrentDownEvent.getDownTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT
- 发送SHOW_PRESS消息的时间:
mCurrentDownEvent.getDownTime() + TAP_TIMEOUT
- 发送TAP消息的延时:
DOUBLE_TAP_TIMEOUT
//上一次焦点的x坐标
private float mLastFocusX;
private float mLastFocusY;
//当前焦点的x坐标
private float mDownFocusX;
private float mDownFocusY;
private MotionEvent mCurrentDownEvent;//当前down事件
private MotionEvent mPreviousUpEvent;//上一次up事件
5.1 MotionEvent.ACTION_POINTER_DOWN
case MotionEvent.ACTION_POINTER_DOWN:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Cancel long press and taps
cancelTaps();
break;
private void cancelTaps() {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mIsDoubleTapping = false;
mAlwaysInTapRegion = false;
mAlwaysInBiggerTapRegion = false;
mDeferConfirmSingleTap = false;
if (mInLongPress) {
mInLongPress = false;
}
}
5.2 MotionEvent.ACTION_POINTER_UP
case MotionEvent.ACTION_POINTER_UP:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
break;
5.3 MotionEvent.ACTION_DOWN
如果当前handler有TAP消息,移除所有的TAP消息
判断是否是双击,如果是,设置回调onDoubleTap和onDoubleTapEvent。如果不是,发送一个延时消息TAP,延时为DOUBLE_TAP_TIMEOUT。
如果允许长安事件发生,在 mCurrentDownEvent.getDownTime()+TAP_TIMEOUT+LONGPRESS_TIMEOUT 时间发送LONG_PRESS消息。
发送SHOW_PRESS消息,在mCurrentDownEvent.getDownTime() + TAP_TIMEOUT 时间。
源码如下:
case MotionEvent.ACTION_DOWN:
if (mDoubleTapListener != null) {
boolean hadTapMessage = mHandler.hasMessages(TAP);
if (hadTapMessage) mHandler.removeMessages(TAP);
if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
// This is a second tap
mIsDoubleTapping = true;
// Give a callback with the first tap of the double-tap
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
// Give a callback with down event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else {
// This is a first tap
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
}
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
if (mCurrentDownEvent != null) {
mCurrentDownEvent.recycle();
}
mCurrentDownEvent = MotionEvent.obtain(ev);
mAlwaysInTapRegion = true;
mAlwaysInBiggerTapRegion = true;
mStillDown = true;
mInLongPress = false;
mDeferConfirmSingleTap = false;
if (mIsLongpressEnabled) {
mHandler.removeMessages(LONG_PRESS);
mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
}
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
handled |= mListener.onDown(ev);
break;
5.4 MotionEvent.ACTION_MOVE
判断是否正在进行长按事件,是,break,否,向下执行。
判断是否是双击事件,是,回调onDoubleTapEvent()接口,否,向下执行。
判断是否是scroll事件,是,移除所有消息,并且向下执行。
移动距离只否满足双击的条件
case MotionEvent.ACTION_MOVE:
if (mInLongPress) {
break;
}
final float scrollX = mLastFocusX - focusX;
final float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
// Give the move events of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mAlwaysInTapRegion) {
final int deltaX = (int) (focusX - mDownFocusX);
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion = false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
if (distance > mDoubleTapTouchSlopSquare) {
mAlwaysInBiggerTapRegion = false;
}
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
break;
5.5 MotionEvent.ACTION_UP
如果是双击事件,回调onDoubleTapEvent接口
如果正在执行长按事件,移除TAP消息,mInLongPress置为false。mInLongPress为boolean类型的变量,判断是否正在执行长按事件。
如果是移动事件,回调onSingleTapUp接口;
移除SHOW_PRESS和LONG_PRESS消息。
case MotionEvent.ACTION_UP:
mStillDown = false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
// Finally, give the up event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mInLongPress) {
mHandler.removeMessages(TAP);
mInLongPress = false;
} else if (mAlwaysInTapRegion) {
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
} else {
// 这里是判断fling事件的代码,这里暂不讨论
}
if (mPreviousUpEvent != null) {
mPreviousUpEvent.recycle();
}
// Hold the event we obtained above - listeners may have changed the original.
mPreviousUpEvent = currentUpEvent;
mIsDoubleTapping = false;
mDeferConfirmSingleTap = false;
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
break;
5.6 MotionEvent.ACTION_CANCEL
case MotionEvent.ACTION_CANCEL:
cancel();
break;
}
private void cancel() {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mVelocityTracker.recycle();
mVelocityTracker = null;
mIsDoubleTapping = false;
mStillDown = false;
mAlwaysInTapRegion = false;
mAlwaysInBiggerTapRegion = false;
mDeferConfirmSingleTap = false;
if (mInLongPress) {
mInLongPress = false;
}
}
网友评论