前言
工作这段时间以来,陆陆续续看了一些类的源码,但是都没有详细的记录下来,只是在笔记上记录了一些功能性的代码,导致一段时间之后,完完全全忘记了。所以今天打算回顾梳理系统里的手势判断类 GestureDetector 。由于我们的项目是一个拍照类美化的 App,所以在之前的版本需求里,产品提了一个在预览页里加上 A/B 对比图的需求。这里的 A、B 指的是原图和添加了滤镜之后的图片,拍照进入预览页显示原图A,但是长按一小段时间之后显示加了滤镜的图片B。当时第一时间想到的就是使用 GestureDetector ,但是预览页的自定义 View 已经有了自己的事件拦截处理的逻辑,不想加上 GestureDetector 使代码更为复杂,所以便萌生了参考 GestureDetector 的想法。
概述
当我们的手指接触到屏幕的时候,会产生许多事件,例如 down,move,up,scroll,fling 等等,一般情况下, 如果我们需要捕获这些事件做一些简单的出来,只需要重写 View 的 onTouchEvent(event) 即可,但是如果是一些比较复杂的手势,比如单击,双击,长按,左右滑动,快速滑动,就需要我们去做一些额外的判断逻辑了,所以系统给我们提供了 GestureDetector ,以此简化我们的开发工作。接下来看一下系统提供的 GestureDetector .OnGestureListener 接口,以下接口方法中,返回 True 代码事件被消费了。源码分析基于android- 25!
-
onDown(MotionEvent e)
当我们触碰到屏幕 Down 事件发生时,会立即回调此方法。
-
onShowPress(MotionEvent e)
用户已经触发了 Down 事件,但是并没有触发 Move 或者 Up 事件。这方法通常是作为一种反馈,让用户知道他手指按下屏幕这一动作已经被识别了。
-
onSingleTapUp(MotionEvent e)
当一个以 Up 事件结束的单击发生时,回调此方法。
-
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
当一个滑动事件发生时回调此方法,其中 e1 代表滑动开始的第一个 Down 事件,e2 代表当前手指正在滑动的 Move 事件,distanceX 代表在 X 轴上前一次滑动和当次滑动的差值量,注意,distanceX 并不是 e1 和 e2 在 X 方向上的差值量。distanceY 同 distanceX 类似,只不过代表的是 Y 轴。
-
onLongPress(MotionEvent e)
长按事件发生时回调此方法
-
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
快速滑动发生时回调此方法,其中 e1 代表触发这个动作的 Down 事件,e2 代表这个动作结束手指抬起的 Up 事件, velocityX 代表在 X 方向上的滑动速度,velocityY 代表在 Y 方向上的滑动速度,数值单位均为 pixels /s ,即每秒划过多少个像素。
在 GestureDetector 类中,我们还发现一个用于确认单击以及双击的接口 GestureDetector .OnDoubleTapListener
-
onSingleTapConfirmed(MotionEvent e)
当确认一个单击事件发生时回调此方法,也就是说,手势判断器判断到这是一次单击事件,用户不会再次点击屏幕。
-
onDoubleTap(MotionEvent e)
当双击事件发生时回调此方法
-
onDoubleTapEvent(MotionEvent e)
双击事件发生过程中都会回调此方法,包括 Down 、Move 、Up 事件。
源码分析
1.构造方法
public GestureDetector(OnGestureListener listener) {
this(null, listener, null);
}
public GestureDetector(Context context, OnGestureListener listener) {
this(context, listener, null);
}
public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
if (handler != null) {
//初始化Handler,用于处理延时消息
mHandler = new GestureHandler(handler);
} else {
mHandler = new GestureHandler();
}
//设置回调监听器
mListener = listener;
if (listener instanceof OnDoubleTapListener) {
setOnDoubleTapListener((OnDoubleTapListener) listener);
}
if (listener instanceof OnContextClickListener) {
setContextClickListener((OnContextClickListener) listener);
}
init(context);
}
private void init(Context context) {
if (mListener == null) {
throw new NullPointerException("OnGestureListener must not be null");
}
mIsLongpressEnabled = true;
// Fallback to support pre-donuts releases
int touchSlop, doubleTapSlop, doubleTapTouchSlop;
if (context == null) {
//noinspection deprecation
touchSlop = ViewConfiguration.getTouchSlop();
doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
//noinspection deprecation
mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
} else {
final ViewConfiguration configuration = ViewConfiguration.get(context);
//滑动的时候,滑动数值大于这个值才认为滑动开始
touchSlop = configuration.getScaledTouchSlop();
doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
//双击位置之间的最大距离,大于这个数值不认为是双击
doubleTapSlop = configuration.getScaledDoubleTapSlop();
//手指抛的最小速度
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
//手指抛的最大速度
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
}
//计算平方,后面用到
mTouchSlopSquare = touchSlop * touchSlop;
mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
}
从以上可以看出,构造 GestureDetector 的过程中,主要做的就是初始化一些变量,并且获取一些数值作为阈值。此外,还需要注意到 GestureHandler ,手势的
private class GestureHandler extends Handler {
//...忽略构造函数
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_PRESS:
//回调 onShowPress 方法
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
}
}
}
2. 处理手势信息
接下来也就到了 GestureDetector 的核心部分,也就是如何处理手势信息,并判断是具体的哪种手势
public boolean onTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
}
final int action = ev.getAction();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
//初始化速度追踪器,并计算速度
mVelocityTracker.addMovement(ev);
//下面需要得出 X 、Y 方向上的中心焦点,如果是多点触碰,需要计算平均值得出中心焦点
// 这里判断这个 action 是不是有非主要的手指抬起来了,如果是的话,记录它的index,下面计算的时候
//忽略这个触点
final boolean pointerUp =
(action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
// 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);
}
//pointerUp 为 true,需要排除掉
final int div = pointerUp ? count - 1 : count;
final float focusX = sumX / div;
final float focusY = sumY / div;
boolean handled = false;
//根据不同的 action,做不同的处理
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
//...
break;
case MotionEvent.ACTION_POINTER_UP:
//...
break;
case MotionEvent.ACTION_DOWN:
//...
break;
case MotionEvent.ACTION_MOVE:
//...
break;
case MotionEvent.ACTION_UP:
//...
break;
case MotionEvent.ACTION_CANCEL:
//...
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
}
return handled;
}
从以上可以看出,onTouchEvent() 方法主要统一对不同的输入事件作统一的处理。
2.1 Down 事件处理
case MotionEvent.ACTION_DOWN:
if (mDoubleTapListener != null) {
//如果设置了双击监听器,进入此结构体
boolean hadTapMessage = mHandler.hasMessages(TAP);
//第一次点击的时候发送了TAG类型的消息,取消掉,防止出错
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
// 这是一个第一次的单击,先发送延时消息,如果在这段时间之内没有再次点击,GestureHandler 里就会处理 这个 TAG 类型的消息
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) {
//允许长按,发送延时的长按消息,在 TAP_TIMEOUT + LONGPRESS_TIMEOUT 这段时间之内,
//如果没有其他手势操作(移动手指、抬起手指),会认为是长按手势,并且回调LongPress 方法。
mHandler.removeMessages(LONG_PRESS);
mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
+ TAP_TIMEOUT + LONGPRESS_TIMEOUT);
}
//一小段时间之后,回调 onShowPress(MotionEvent e) 方法,反馈给用户
mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
handled |= mListener.onDown(ev);
break;
//判断第二次点击是否有效
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();
//范围没超过mDoubleTapSlopSquare ,认为有效
return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
}
Down 事件总结:
- 单击判断:如果接受到一次单击事件,会先发送 TAG 类型的延迟消息,在这段时间内,如果消息没被取消,则会进入 GestureHandler 的 TAG 确认操作。
- 双击判断:已经有了第一次的单击事件,这时候需要判断第二次点击和第一次单击的距离、时间,条件符合则认为是双击,回调相关方法。
- 长按判断:点击的时候就发送延迟的长按消息,要是这段时间内没被取消,就回调长按的方法。
2.2 Move 事件处理
case MotionEvent.ACTION_MOVE:
//已经触发长按操作,一系列事件可以认为是长按,接下来不做其他处理
if (mInLongPress || mInContextClick) {
break;
}
final float scrollX = mLastFocusX - focusX;
final float scrollY = mLastFocusY - focusY;
if (mIsDoubleTapping) {
// Give the move events of the double-tap
//上面说过,双击过程中 onDoubleTapEvent(ev) 方法会被回调,这个过程还没结束
//回调这个方法
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mAlwaysInTapRegion) {
//第一次 Down 事件里,mAlwaysInTapRegion 为 true
final int deltaX = (int) (focusX - mDownFocusX);
final int deltaY = (int) (focusY - mDownFocusY);
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare) {
// 距离超过一个数值,可以认为是滑动,进入onScroll 模式,回调相关函数
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)) {
//最后的条件判断,继续 onScroll 回调
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastFocusX = focusX;
mLastFocusY = focusY;
}
break;
Move 事件总结:
- 如果已经触发了长按,并且回调了长按的相关函数,就会认为接下来的 Move 事件都会直接返回。
- 如果是双击,因为双击过程还未结束,所以 Move 事件会一直回调 onDoubleTapEvent(ev) 方法。
- 如果滑动的距离超过一定的数值平方,会进入 onScroll 模式,接下来的滑动,只要超过 1px,就继续回调 onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 方法。
2.3 Up 事件处理
case MotionEvent.ACTION_UP:
mStillDown = false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping) {
// Finally, give the up event of the double-tap
//是双击的过程,回调onDoubleTapEvent(ev) 方法
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
} else if (mInLongPress) {
//长按的过程,移除可能存在的 TAP 消息
mHandler.removeMessages(TAP);
mInLongPress = false;
} else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
//如果点击之后没有移动,或者移动的距离没有超过一定的数值,可以认为只是单击而已
handled = mListener.onSingleTapUp(ev);
if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
//GestureHandler 里 mDeferConfirmSingleTap 被设置为 true,这里在手指抬起时进行回调
mDoubleTapListener.onSingleTapConfirmed(ev);
}
} else if (!mIgnoreNextUpEvent) {
//进入这个结构体的时候,说明前面没有触发双击或者长按操作,并且滑动的距离也超过了最小值,
//mAlwaysInTapRegion 被设置为 false
// 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);
}
}
//回收,方便下面使用
if (mPreviousUpEvent != null) {
mPreviousUpEvent.recycle();
}
// Hold the event we obtained above - listeners may have changed the original.
mPreviousUpEvent = currentUpEvent;
if (mVelocityTracker != null) {
// This may have been cleared when we called out to the
// application above.
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mIsDoubleTapping = false;
mDeferConfirmSingleTap = false;
mIgnoreNextUpEvent = false;
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
break;
Up 事件总结:
- 判断是双击的过程,则继续回调 onDoubleTapEvent(ev) 方法,结束这个过程.
- 如果是长按的,也结束这个过程。
- 说明前面没有触发双击或者长按操作,并且滑动的距离也超过了最小值,最后手指抬起时,速度超过最小值,可以认为是抛的动作,回调 onFling() 方法。
- 如果点击之后没有移动,或者移动的距离没有超过一定的数值,认为只是简单的点击,结束这个过程。之前 点击的时候 发生了延迟的 TAG消息,但是需要手指抬起才能回调 onSingleTapConfirmed() 方法,如果处理延迟消息的时候,手指还未抬起,则先设置变量标记(处理逻辑如下图),等手指抬起再回调。
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;
}
}
2.4 多点触控处理
case MotionEvent.ACTION_POINTER_DOWN:
mDownFocusX = mLastFocusX = focusX;
mDownFocusY = mLastFocusY = focusY;
// Cancel long press and taps
//有其他手指落下,移除所有消息,重置标记变量
cancelTaps();
break;
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);
//如果剩下的手指速度方向是和抬起那根手指的速度相反方向的,就说明不是fling,清空速度监听
final float dot = x + y;
if (dot < 0) {
mVelocityTracker.clear();
break;
}
}
break;
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;
}
在 onTouchEvent(MotionEvent ev) 方法的前面,我们对于多点触控的处理都是取平均值的,对多个手指的UP和DOWN事件处理,其实就是做一些取消操作而让多点触摸不影响单点触摸的应用,例如在多个手指落下的时候取消点击信息等。
后记
第一次分析源码,如果有错误或者需要补充的,欢迎留言讨论。
网友评论