1 简介
手势检测器GestureDetector用于帮助开发者辨别一些基本的触摸手势,如点击、长按、滑动等。这使得开发者可以专注于业务处理,不用再花精力去处理手势识别相关逻辑。
GestureDetector类包含三个监听接口OnGestureListener, OnDoubleTapListener, OnContextClickListener,一个外部类SimpleOnGestureListener,一个内部类GestureHandler。其中OnGestureListener会监听单击,滑动,长按,fling等,OnDoubleTapListener监听双击和单击事件,OnContextClickListener用于外接设备。 SimpleOnGestureListener是实现了OnGestureListener, OnDoubleTapListener, OnContextClickListener的空类,使用时,一般继承SimpleOnGestureListener就可以了。
2 接口介绍
public interface OnGestureListener {
boolean onDown(MotionEvent e);
void onShowPress(MotionEvent e);
boolean onSingleTapUp(MotionEvent e);
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
void onLongPress(MotionEvent e);
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
boolean onContextClick(MotionEvent e);
}
3 源码分析
GestureDetector中的逻辑处理可以拆解成三部分,第一部分是在onTouchEvent中的逻辑处理和回调方法,第二部分是通过handler的回调方法,第三部分是控制变量相关的处理。
GestureDetector的核心逻辑是放在onTouchEvent中的,我们先来看下onTouchEvent的结构:
/**
* Analyzes the given motion event and if applicable triggers the
* appropriate callbacks on the {@link OnGestureListener} supplied.
*
* @param ev The current motion event.
* @return true if the {@link OnGestureListener} consumed the event,
* else false.
*/
public boolean onTouchEvent(MotionEvent ev) {
...
final int action = ev.getAction();
...
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
final boolean pointerUp =
(action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
final boolean isGeneratedGesture =
(ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = ev.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i) continue;
sumX += ev.getX(i);
sumY += ev.getY(i);
}
final int div = pointerUp ? count - 1 : count;
final float focusX = sumX / div;
final float focusY = sumY / div;
boolean handled = false;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
...
case MotionEvent.ACTION_POINTER_UP:
...
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_MOVE:
...
case MotionEvent.ACTION_UP:
...
case MotionEvent.ACTION_CANCEL:
...
}
...
return handled;
}
先是获取事件坐标(focusX, focusY),如果是多点触控的话,取的是所有触控点(排除抬起点ACTION_POINTER_UP)的平均值。接着进入到各个action处理中。先来看onTouchEvent中直接回调是怎么处理的:
3.1 onTouchEvent中的直接回调
ACTION_DOWN
case MotionEvent.ACTION_DOWN:
if (mDoubleTapListener != null) {
//如果有TAP消息,取消
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;
mHasRecordedClassification = false;
...
handled |= mListener.onDown(ev);
break;
先掠过if (mDoubleTapListener != null)那一段的处理,直接看下面。
先是一堆属性值和控制变量的设置,注意mCurrentDownEvent,它被看作是双击中的第一次点击时的DOWN事件,后面会说到。调用回调onDown()。
现在来看if (mDoubleTapListener != null)那一段,这一段是用来处理双击的逻辑。先判断是否已经发送过TAP消息,mCurrentDownEvent不为空,mPreviousUpEvent是双击中第一次点击时的ACTION_UP事件,isConsideredDoubleTap()从字面意思上看,是判断两次点击是否可以算作一次双击行为。从源码上看,是通过两个条件来判断的,一是第一次点击的UP事件和第二次点击的DOWN事件之间的时间间隔要在DOUBLE_TAP_TIMEOUT和DOUBLE_TAP_MIN_TIME之间;二是第一次点击的DOWN事件点和第二次点击的DOWN事件点的距离要小于doubleTapSlop。如果以上条件都满足了,就认为这次DOWN事件是一次双击行为,会调用回调onDoubleTap()和onDoubleTapEvent()。否则的话,发送空消息TAP,认为是双击中第一次点击行为。
注意:onDoubleTap传入的参数MotionEvent是第一次点击时的DOWN事件,且onDoubleTap只会在此处被调用一次;onDoubleTapEvent()传入的参数MotionEvent是第二次点击时的DOWN事件。确认是双击后,onDoubleTapEvent()会在MOVE事件和UP事件里都会被调用一次。
ACTION_MOVE
case MotionEvent.ACTION_MOVE:
...
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);
int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
...
if (distance > slopSquare) {
...
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion = false;
...
}
...
} else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
...
break;
如果已经在DOWN事件中确认是双击(mIsDoubleTapping为true),会再次调用回调onDoubleTapEvent(ev),此时传入的MotionEvent,是第二次点击的MOVE事件。
否则,判断控制变量mAlwaysInTapRegion是否为true, 为true的话,如果滑动距离(deltaX * deltaX) + (deltaY * deltaY)超出slopSquare, 认为是scroll,调用回调onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY),其中e1是DOWN事件的Motionevent ,e2是MOVE事件的MotionEvent, distanceX和 distanceY是上次滑动点和本次滑动点的距离差。
如果mAlwaysInTapRegion是false,走if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1))分支,调用onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY),参数解释同上。
ACTION_UP
case MotionEvent.ACTION_UP:
...
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
// Finally, give the up event of the double-tap
...
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
...
else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
...
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
mDoubleTapListener.onSingleTapConfirmed(ev);
}
} else if (!mIgnoreNextUpEvent) {
// A fling must travel the minimum tap distance
final VelocityTracker velocityTracker = mVelocityTracker;
final int pointerId = ev.getPointerId(0);
velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final float velocityY = velocityTracker.getYVelocity(pointerId);
final float velocityX = velocityTracker.getXVelocity(pointerId);
if ((Math.abs(velocityY) > mMinimumFlingVelocity)
|| (Math.abs(velocityX) > mMinimumFlingVelocity)) {
handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
}
}
...
mIsDoubleTapping = false;
mDeferConfirmSingleTap = false;
mIgnoreNextUpEvent = false;
break;
如果已经在DOWN事件中确认是双击(mIsDoubleTapping为true),会再次调用回调onDoubleTapEvent(ev),此时传入的MotionEvent时第二次点击的UP事件。
否则如果mAlwaysInTapRegion为true,调用回调onSingleTapUp(ev);此时如果mDeferConfirmSingleTap为ture,调用回调onSingleTapConfirmed(ev)。mIgnoreNextUpEvent一般是false,可忽略掉。
否则,判定是否是fling状态,获取velocityX, velocityY, 如果有一个超过给定的最小fling值,认为是fling, 调用回调onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); e1是本次Down事件的MotionEvent,e2是本次UP事件的MotionEvent, velocityX, velocityY是当前的x,y方向的速度值。
ACTION_POINTER_DOWN
case MotionEvent.ACTION_POINTER_DOWN:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Cancel long press and taps
cancelTaps();
break;
处理的是多点触控中的POINTER_DOWN事件,重新赋值mDownFocusX, mDownFocusY, mLastFocusX, mLastFocusY。cancelTaps()作用是remove掉所有的message和重置所有和tap相关的控制变量。
ACTION_POINTER_UP
case MotionEvent.ACTION_POINTER_UP:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Check the dot product of current velocities.
// If the pointer that left was opposing another velocity vector, clear.
mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
final int upIndex = ev.getActionIndex();
final int id1 = ev.getPointerId(upIndex);
final float x1 = mVelocityTracker.getXVelocity(id1);
final float y1 = mVelocityTracker.getYVelocity(id1);
for (int i = 0; i < count; i++) {
if (i == upIndex) continue;
final int id2 = ev.getPointerId(i);
final float x = x1 * mVelocityTracker.getXVelocity(id2);
final float y = y1 * mVelocityTracker.getYVelocity(id2);
final float dot = x + y;
if (dot < 0) {
mVelocityTracker.clear();
break;
}
}
break;
考虑的是多点触控模式下,手指抬起时的状态。重置mDownFocusX, mDownFocusY, mLastFocusX, mLastFocusY。
3.2 通过GestureHandler处理的回调
先来看下handleMessage的处理逻辑
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PRESS:
mListener.onShowPress(mCurrentDownEvent);
break;
case LONG_PRESS:
recordGestureClassification(msg.arg1);
dispatchLongPress();
break;
case TAP:
// If the user's finger is still down, do not count it as a tap
if (mDoubleTapListener != null) {
if (!mStillDown) {
recordGestureClassification(
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
} else {
mDeferConfirmSingleTap = true;
}
}
break;
default:
throw new RuntimeException("Unknown message " + msg); //never
}
}
从代码中看出来,handleMessage中关联的回调方法是onShowPress(),onLongPress(),onSingleTapConfirmed()。
先来看下三种message的发送和取消的时机。
Public boolean onTouchEvent(MotionEvent ev) {
case MotionEvent.ACTION_POINTER_DOWN:
//Cancel long press and taps
cancelTaps();
...
case MotionEvent.ACTION_DOWN:
...
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;
...
} else {
// This is a first tap
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
...
mAlwaysInTapRegion = true;
mStillDown = true;
mInLongPress = false;
...
if (mIsLongpressEnabled) {
mHandler.removeMessages(LONG_PRESS);
mHandler.sendMessageAtTime(
mHandler.obtainMessage(
LONG_PRESS,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
0 /* arg2 */),
mCurrentDownEvent.getDownTime()
+ ViewConfiguration.getLongPressTimeout());
}
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime()+TAP_TIMEOUT);
case MotionEvent.ACTION_MOVE:
if (mInLongPress || mInContextClick) {
break;
}
if (mIsDoubleTapping) {
...
} else if (mAlwaysInTapRegion) {
final int deltaX = (int) (focusX - mDownFocusX);
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
...
if (distance > slopSquare) {
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
mAlwaysInTapRegion = false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
}
case MotionEvent.ACTION_UP:
mStillDown = false;
...
if (mIsDoubleTapping) {
...
} else if (mInLongPress) {
mHandler.removeMessages(TAP);
mInLongPress = false;
}
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
case MotionEvent.ACTION_CANCEL:
cancel();
}
private void dispatchLongPress() {
mHandler.removeMessages(TAP);
mDeferConfirmSingleTap = false;
mInLongPress = true;
mListener.onLongPress(mCurrentDownEvent);
}
private void cancelTaps() {
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mIsDoubleTapping = false;
mAlwaysInTapRegion = false;
mAlwaysInBiggerTapRegion = false;
mDeferConfirmSingleTap = false;
mInLongPress = false;
mInContextClick = false;
mIgnoreNextUpEvent = false;
}
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;
mInLongPress = false;
mInContextClick = false;
mIgnoreNextUpEvent = false;
}
SHOW_PRESS
SHOW_PRESS的发送时机是DOWN事件中延时TAP_TIMEOUT发送;UP,CANCEL,ACTION_POINTER_DOWN事件发生就remove掉该消息;此外,在MOVE事件中,如果mAlwaysInTapRegion为true的情况下,调用了onScroll(),也会remove掉该消息。
handleMessage收到该消息,会直接调用onShowPress()
LONG_PRESS
顾名思义,长按,发送的时机是DOWN事件里,如果mIsLongpressEnabled为true,即支持长按,延时ViewConfiguration.getLongPressTimeout()发送;remove时机同SHOW_PRESS。
handleMessage收到该消息,会调用方法dispatchLongPress(),remove掉TAP消息,设置控制变量mDeferConfirmSingleTap和mInLongPress,调用回调onLongPress(ev)。
private void dispatchLongPress() {
mHandler.removeMessages(TAP);
mDeferConfirmSingleTap = false;
mInLongPress = true;
mListener.onLongPress(mCurrentDownEvent);
}
TAP
在DOWN事件中,判断不是双击,会延时DOUBLE_TAP_TIMEOUT发送该消息。 MOVE,CANCEL,ACTION_POINTER_DOWN事件中的remove行为同SHOW_PRESS,UP事件中,mInLongPress为true的情况下remove掉,如果确认是长按事件了,也会remove掉该消息。
注意,handleMessage中的回调,参数传入的都是DOWN事件的MotionEvent.
3.3控制变量相关
GestureDetector的控制变量主要是以下这几个
private boolean mStillDown;
private boolean mDeferConfirmSingleTap;
private boolean mInLongPress;
private boolean mAlwaysInTapRegion;
private boolean mAlwaysInBiggerTapRegion;
private boolean mIsDoubleTapping;
private boolean mIsLongpressEnabled;
mStillDown的作用是在handleMessage中处理TAP时,控制是否调用回调onSingleTapConfirmed(),只有在mStillDown是false时才可以调用。DOWN事件中它被赋值true,UP和CANCEL事件中赋值false。
mDeferConfirmSingleTap为true表示可以在UP事件中调用onSingleTapConfirmed(ev),而mDeferConfirmSingleTap默认是false,只有在处理TAP消息时,mStillDown为true时,才被赋值为true。
mInLongPress默认为false,只有在处理LONG_PRESS消息时,设为true。为true的时候,不执行MOVE事件。
mAlwaysInTapRegion在DOWN事件赋值为true,ACTION_POINTER_DOWN和CANCEL事件设为false。MOVE事件中,在它为true的条件下调用了onScroll()后,会赋值为false。UP事件中,如果已经是false,说明这次行为是scroll,不是的话,说明是单击,会调用onSingleTapUp()和onSingleTapConfirmed()。
mAlwaysInBiggerTapRegion用于判断是否需要执行isConsideredDoubleTap()方法,这应该是一个优化,DOWN事件中设为true,MOVE事件中如果是scroll行为,设为false,即确认不是双击,不用再走一遍isConsideredDoubleTap()了。
mIsDoubleTapping标志是否双击,DOWN事件时确定是双击后,设为true; UP事件时重置,设为false。ACTION_POINTER_DOWN和CANCEL事件同样重置,设为false。它主要用来判断要不要在MOVE事件和UP事件时,调用onDoubleTapEvent()。
mIsLongpressEnabled默认是true,即默认是支持长按的,你也可以通过setIsLongpressEnabled()禁用长按。它的作用是在DOWN事件中控制是否要发送LONG_PRESS消息。
总结
通过上面的源码拆解分析,可以看出来GestureDetector如何通过控制变量,handler和MotionEvent共同来判断触摸行为的。
DOWN事件必然会调用的回调是onDown(),确认是双击后,可能会调用的是onDoubleTap()和onDoubleTapEvent(),之后MOVE和UP事件会各调用一次onDoubleTapEvent()。
MOVE事件先判断是否是长按(mInLongPress),再判断是否是双击(onDoubleTapEvent),接着判断是否是滑动(onScroll)
UP事件先判断是否是双击(mIsDoubleTapping,onDoubleTapEvent()),再判断是否是长按(mInLongPress),接着判断是否是单击(mAlwaysInTapRegion,onSingleTapUp,onSingleTapConfirmed),最后再判断是不是fling(onFling)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。