Android手势检测——GestureDetector全面分析

2023-11-05 11:38

本文主要是介绍Android手势检测——GestureDetector全面分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

http://blog.csdn.net/totond/article/details/77881180

  在很多视频播放器中,都存在使用不同的手势来控制进度、亮度\音量和暂停播放等功能。Android提供了一个GestureDetector来帮助我们识别一些基本的触摸手势(还有ScaleGestureDetector可以识别缩放手势),让我们很方便地实现手势控制功能。下面我们就来学习一下GestureDetector的使用和通过源码(Android7.0)来分析一下它的实现,让我们对触摸事件处理的理解更加深入。

GestureDetector介绍

  Detector的意思就是探测者,所以GestureDetector就是用来监听手势的发生。它内部有3个Listener接口,用来回调不同类型的触摸事件,用一个简略的类图来显示:

 
   
  里面这些接口的方法,就是相应触摸事件的回调,实现了这些方法,就能实现传入触摸事件之后做出相应的回调。

一些回调接口:

1.OnGestureListener,这个Listener监听一些手势,如单击、滑动、长按等操作: 
onDown(MotionEvent e):用户按下屏幕的时候的回调。 
onShowPress(MotionEvent e):用户按下按键后100ms(根据Android7.0源码)还没有松开或者移动就会回调,官方在源码的解释是说一般用于告诉用户已经识别按下事件的回调(我暂时想不出有什么用途,因为这个回调触发之后还会触发其他的,不像长按)。 
onLongPress(MotionEvent e):用户长按后(好像不同手机的时间不同,源码里默认是100ms+500ms)触发,触发之后不会触发其他回调,直至松开(UP事件)。 
onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY):手指滑动的时候执行的回调(接收到MOVE事件,且位移大于一定距离),e1,e2分别是之前DOWN事件和当前的MOVE事件,distanceX和distanceY就是当前MOVE事件和上一个MOVE事件的位移量。 
onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY):用户执行抛操作之后的回调,MOVE事件之后手松开(UP事件)那一瞬间的x或者y方向速度,如果达到一定数值(源码默认是每秒50px),就是抛操作(也就是快速滑动的时候松手会有这个回调,因此基本上有onFling必然有onScroll)。 
onSingleTapUp(MotionEvent e):用户手指松开(UP事件)的时候如果没有执行onScroll()onLongPress()这两个回调的话,就会回调这个,说明这是一个点击抬起事件,但是不能区分是否双击事件的抬起。

2.OnDoubleTapListener,这个Listener监听双击和单击事件。 
onSingleTapConfirmed(MotionEvent e):可以确认(通过单击DOWN后300ms没有下一个DOWN事件确认)这不是一个双击事件,而是一个单击事件的时候会回调。 
onDoubleTap(MotionEvent e):可以确认这是一个双击事件的时候回调。 
onDoubleTapEvent(MotionEvent e):onDoubleTap()回调之后的输入事件(DOWN、MOVE、UP)都会回调这个方法(这个方法可以实现一些双击后的控制,如让View双击后变得可拖动等)。

3.OnContextClickListener,很多人都不知道ContextClick是什么,我以前也不知道,直到我把平板接上了外接键盘——原来这就是鼠标右键。。。 
onContextClick(MotionEvent e):当鼠标/触摸板,右键点击时候的回调。

4.SimpleOnGestureListener,实现了上面三个接口的类,拥有上面三个的所有回调方法。 
- 由于SimpleOnGestureListener不是抽象类,所以继承它的时候只需要选取我们所需要的回调方法来重写就可以了,非常方便,也减少了代码量,符合接口隔离原则,也是模板方法模式的实现。而实现上面的三个接口中的一个都要全部重写里面的方法,所以我们一般都是选择SimpleOnGestureListener。

ps:上面所有的回调方法的返回值都是boolean类型,和View的事件传递机制一样,返回true表示消耗了事件,flase表示没有消耗。

GestureDetector的使用

  GestureDetector的使用很简单,因为它的功能就是定义为识别手势,所以使用的话就是输入完整的触摸事件(完整的意思就是用户所有的触摸操作都是输入给它。为什么要强调完整,因为我上一篇博客就是分享如何拦截子View的部分触摸事件),识别然后进行相应的回调:

    private void init(Context context){mOnGestureListener = new MyOnGestureListener();mGestureDetector = new GestureDetector(context,mOnGestureListener);
//        mGestureDetector.setIsLongpressEnabled(false);setOnTouchListener(new OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {//监听触摸事件return mGestureDetector.onTouchEvent(event);}});setOnGenericMotionListener(new OnGenericMotionListener() {@Overridepublic boolean onGenericMotion(View v, MotionEvent event) {Log.d(TAG, "onGenericMotion: ");//监听鼠标右键点击事件return mGestureDetector.onGenericMotionEvent(event);}});}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

如上面的代码,要使用OnGestureListener和OnDoubleTapListener里面的回调需要调用GestureDetector.onTouchEvent()方法,而使用OnContextClickListener的话则是需要调用onGenericMotionEvent()方法,注意一个是在onTouch()方法一个是在onGenericMotion()方法。

  看完了上面一堆文字,其实你就会懂得如何使用GestureDetector了,这里有GestureDetector的实践。但是如果你想了解它的回调的时机为什么会是这样的,想具体了解它们的回调时机,可以继续看下去,下面是源码分析。

GestureDetector源码分析

1. 初始化处理

  GestureDetector的源码接近800行,这在Android源码中已经算是比较短的了(毕竟注释也占一两百行了),所以说它的实现也不是很复杂的。从它的构造方法开始:

    public GestureDetector(Context context, OnGestureListener listener) {this(context, listener, null);}public GestureDetector(Context context, OnGestureListener listener, Handler handler) {//初始化Handlerif (handler != null) {mHandler = new GestureHandler(handler);} else {mHandler = new GestureHandler();}//设置ListenermListener = 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 releasesint touchSlop, doubleTapSlop, doubleTapTouchSlop;if (context == null) {//相当于下面的getScaledTouchSlop,表示滑动的时候,手的移动要大于这个距离才开始移动控件touchSlop = ViewConfiguration.getTouchSlop();//相当于下面的getScaledDoubleTapTouchSlop,表示点击的时候,手指移动大于这个距离,就被认为不可能是双击doubleTapTouchSlop = touchSlop; //相当于下面的getScaledDoubleTapSlop,表示第二次点击的时候,和第一次的点击点位置距离如果大于这个,就被认为不是双击doubleTapSlop = ViewConfiguration.getDoubleTapSlop();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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

  可见GestureDetector的创建就是初始化一些属性,然后就是把对应的Listener设置好,还有初始化Handler,而这里的GestureHandler,是控制onShowPress()onLongPress(),onSingleTapConfirmed()`回调的关键:

  private class GestureHandler extends Handler {//省略构造函数...@Overridepublic 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//这里控制SingleTapConfirmed的回调,if (mDoubleTapListener != null) {if (!mStillDown) {//如果已经松开,就立刻调用SingleTapConfirmedmDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);} else {//如果处理Message的时候还没松开,就设置mDeferConfirmSingleTap为true,在UP事件的时候调用SingleTapConfirmemDeferConfirmSingleTap = true;}}break;default:throw new RuntimeException("Unknown message " + msg); //never}}}//长按处理private void dispatchLongPress() {mHandler.removeMessages(TAP);mDeferConfirmSingleTap = false;mInLongPress = true;mListener.onLongPress(mCurrentDownEvent);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

2. 输入处理

  初始化完之后,就是看它的如何处理输入了,这是它的核心逻辑:

    public boolean onTouchEvent(MotionEvent ev) {//检查事件输入的一致性,log出来一致性的信息,如:有事件只有up没有downif (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(ev, 0);}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;// Determine focal point// 是非主要指针抬起动作的话会跳过float sumX = 0, sumY = 0;final int count = ev.getPointerCount();//把所有还在触摸的手指的位置x,y加起来,后面求平均数,算出中心焦点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://...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:cancel();break;}//对未被处理的事件进行一次一致性检测if (!handled && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);}return handled;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

  上面的注释写得很清楚了,主要onTouchEvent()的主要思路就是先对输入事件做出统一处理,提取一些共有的信息,如多个点同时触摸时候的中心焦点和滑动速度等,然后根据事件的分类做出相应的处理。

ps:InputEventConsistencyVerifier对输入事件进行的一致性检测的结果并不影响GestureDetector的运行,如果检测到一致性不符合的事件(只有UP事件而前面没有DOWN事件),就只会输出log告诉开发者。

2.1. DOWN事件处理

  下面进入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 tapmIsDoubleTapping = 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-taphandled |= mDoubleTapListener.onDoubleTapEvent(ev);} else {// This is a first tap//延时发出单击事件,如果到了时间(300ms)还没有取消的话就确认是TAP事件了mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);}}mDownFocusX = mLastFocusX = focusX;mDownFocusY = mLastFocusY = focusY;//重置mCurrentDownEventif (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);}//延时发送showPress事件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();//判断第二次点击是否在附近,在附近才被认为是双击return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

  可见,对DOWN事件涉及: 
- 处理单击判断:如果收到一次DOWN事件,而且前段时间没有DOWN事件的话,会发送一个延时的TAP信息,而一段时间(300ms)之后没有被取消的话,就执行GestureHandler里面的TAP单击确认操作。 
- 处理双击判断:如果前面也有一次DOWN事件,而且也符合isConsideredDoubleTap()的条件(第一次点击后没有移动超出范围,第二次点击也在附近),就可以确认双击,执行onDoubleTap()onDoubleTapEvent()的回调。 
- 处理长按判断:先看用户是否允许检测长按,然后就是发送一个延时的LONG_PRESS信息,如果到时候还没被取消的话就是回调长按方法了。 
- 处理showPress判断:这个和长按差不多,就是时间(100ms)短了一点而已。

PS:handled是boolean变量,|=符号是用符号右边的值跟左边的值进行或运算再赋值给左边。a |= b等价于a = a | b,这里使handled变量初始值为false,进行了多次|=操作,一旦有结果是true的话,handled最后的值就是true,所有结果都是false最后才会false,onTouchEvent()方法最后返回这个handled,就可以表示事件有没被处理,实现了对事件处理的封装。

2.2. MOVE事件处理

  然后再看看对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//如果是第二次点击的话,把移动事件也当作双击,有点奇怪handled |= mDoubleTapListener.onDoubleTapEvent(ev);} else if (mAlwaysInTapRegion) {//down才会使mAlwaysInTapRegion为truefinal int deltaX = (int) (focusX - mDownFocusX);final int deltaY = (int) (focusY - mDownFocusY);int distance = (deltaX * deltaX) + (deltaY * deltaY);//mTouchSlopSquare是一个距离的平方,表示滑动的时候,手的移动要大于这个距离才认为是Scroll事件if (distance > mTouchSlopSquare) {//进入Scroll模式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)) {//后续的Scroll移动,前面的是进入Scroll移动handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);mLastFocusX = focusX;mLastFocusY = focusY;}break;//...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

  可见,对MOVE事件涉及: 
onDoubleTapEvent()回调:只要确认是双击之后,mIsDoubleTapping为true,除了长按,后面的MOVE事件都会只回调onDoubleTapEvent()。 
onScroll()回调:当MOVE不是长按,不是DoubleTapEvent之后,当移动距离大于一定距离之后,就会进入Scroll模式,然后两个MOVE事件的位移距离scrollX或者scrollY大于1px,都会调用onScroll()

2.3. UP事件的处理

  接下来再看UP事件:

    //...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 && !mIgnoreNextUpEvent) {handled = mListener.onSingleTapUp(ev);//处理单击确认,具体逻辑看GestureHandler如何处理TAP事件if (mDeferConfirmSingleTap && mDoubleTapListener != null) {mDoubleTapListener.onSingleTapConfirmed(ev);}} else if (!mIgnoreNextUpEvent) {//处理Fling,如果速度大于定义的最小速度(50),就回调Fling// A fling must travel the minimum tap distancefinal 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);}}//重置mPreviousUpEventif (mPreviousUpEvent != null) {mPreviousUpEvent.recycle();}// Hold the event we obtained above - listeners may have changed the original.mPreviousUpEvent = currentUpEvent;//回收mVelocityTrackerif (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;//...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

  可见,对MOVE事件涉及: 
onDoubleTapEvent()回调:只要确认是双击之后,mIsDoubleTapping为true,除了长按,后面的MOVE事件都会只回调onDoubleTapEvent()。 
onSingleTapUp()回调:DOWN事件之后没有MOVE,或者MOVE的距离没有超出范围,mAlwaysInTapRegion才不会变成false,回调onSingleTapUp()。 
onSingleTapConfirmed()回调:从前面GestureHandler里面的TAP消息的实现可以看到:

            case TAP:// If the user's finger is still down, do not count it as a tap//这里控制SingleTapConfirmed的回调,if (mDoubleTapListener != null) {if (!mStillDown) {//如果已经松开,就立刻调用SingleTapConfirmedmDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);} else {//如果处理Message的时候还没松开,就设置mDeferConfirmSingleTap为true,在UP事件的时候调用SingleTapConfirmemDeferConfirmSingleTap = true;}}break;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

  之前看过,TAP消息是延时(300ms)发送的,然而实际逻辑中,是抬起手指才算是点击,所以这里处理TAP的时候就不一定立刻调用onSingleTapConfirmed(),而是判断手指是否松开了,是松开的话就立刻回调。如果还未松开那就把标志位mDeferConfirmSingleTap设置为true,等到收到UP事件的时候再回调。 
onFling()回调:当UP事件的速度大于一定速度时,就会回调onFling(),至于mIgnoreNextUpEvent参数,是只有鼠标右键点击的时候才会为true,具体看后面。

2.4. 多点触摸的处理

  对于多个手指落下,前面的统一处理已经是把所有手指的坐标值加起来然后算平均值了,所以我们多根手指触摸的时候,其实滑动的实际点是这些手指的中心焦点。回看上面的MOVE事件处理,中心焦点的位置值FocusX和FocusY决定onScroll(onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY))的后两个参数值,所以处理多点触控的话就使用它们比较方便。因为MotionEvent是使用数组装着当前屏幕上所有指针的动作的,使用前两个参数的话还要循环用getX(int pointerIndex)getY(int pointerIndex)方法取出各个指针的值再自己处理。

    //...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);//如果剩下的手指速度方向是和抬起那根手指的速度相反方向的,就说明不是fling,清空速度监听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;//...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;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

  这是对多个手指的UP和DOWN事件处理,其实就是做一些取消操作而让多点触摸不影响单点触摸的应用,例如在多个手指落下的时候取消点击信息等。

2.5. ContextClick的处理

  ContextClick的事件是由onGenericMotionEvent()传入:

    public boolean onGenericMotionEvent(MotionEvent ev) {if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);}final int actionButton = ev.getActionButton();switch (ev.getActionMasked()) {case MotionEvent.ACTION_BUTTON_PRESS://按下触控笔首选按钮或者鼠标右键if (mContextClickListener != null && !mInContextClick && !mInLongPress&& (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY|| actionButton == MotionEvent.BUTTON_SECONDARY)) {if (mContextClickListener.onContextClick(ev)) {mInContextClick = true;mHandler.removeMessages(LONG_PRESS);mHandler.removeMessages(TAP);return true;}}break;case MotionEvent.ACTION_BUTTON_RELEASE:if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY|| actionButton == MotionEvent.BUTTON_SECONDARY)) {mInContextClick = false;//无视下一个UP事件,因为它是由鼠标右键或者触控笔键带起的mIgnoreNextUpEvent = true;}break;}return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

  由此可以,当按键按下(ACTION_BUTTON_PRESS)的时候已经回调onContextClick()

总结

  读完源码,总结出来的每个回调的调用时机如下表:

PS:除去onContextClick(),因为它的按下鼠标右键时候是发出一系列的事件。

回调/输入事件 DOWN事件 MOVE事件 UP事件
onDown(MotionEvent e) × ×
onShowPress(MotionEvent e) × ×
onLongPress(MotionEvent e) × ×
onScroll(MotionEvent e1, MotionEvent e2,float distanceX, float distanceY) × ×
onFling(MotionEvent e1, MotionEvent e2, float velocityX,float velocityY)) × ×
onSingleTapUp(MotionEvent e) × ×
onSingleTapConfirmed(MotionEvent e) × ×
onDoubleTap(MotionEvent e) × ×
onDoubleTapEvent(MotionEvent e)

  从上面的分析可以看出,虽然GestureDetector能识别很多手势,但是也是不能满足所有的需求的,如滑动和长按之后松开没有回调(这个可以重写onTouch()捕捉UP事件实现)、多点触控缩放手势的实现(这个可以用ScaleGestureDetector)等。

后话

  有人问我看GestureDetector源码这么仔细有什么用,它又不是很常用的东西,网上随便一搜一堆资料。我的回答是因为我觉得要用一个东西的话,首先就是要搞清楚它能干什么,它的限制是什么,为什么要选择它,关于这些方面,网上的很多关于GestureDetector的资料都没有达到我想了解的程度,加上GestureDetector并不复杂,所以写下了这篇博客,这样就可以从源码层面上了解到它的回调是什么时候调用,有bug的时候也能更快的找出。 
  不管怎样,GestureDetector里面的SimpleOnGestureListener的设计,和对触摸事件的处理方式是很值得我学习的,记录分享至此,水平有限,如果错漏,欢迎指正和讨论。

这篇关于Android手势检测——GestureDetector全面分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/349596

相关文章

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis主从复制的原理分析

《Redis主从复制的原理分析》Redis主从复制通过将数据镜像到多个从节点,实现高可用性和扩展性,主从复制包括初次全量同步和增量同步两个阶段,为优化复制性能,可以采用AOF持久化、调整复制超时时间、... 目录Redis主从复制的原理主从复制概述配置主从复制数据同步过程复制一致性与延迟故障转移机制监控与维

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

Spring中Bean有关NullPointerException异常的原因分析

《Spring中Bean有关NullPointerException异常的原因分析》在Spring中使用@Autowired注解注入的bean不能在静态上下文中访问,否则会导致NullPointerE... 目录Spring中Bean有关NullPointerException异常的原因问题描述解决方案总结

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异