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

出处
炎之铠邮箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原创,转载请注明本出处!

前言

在很多视频播放器中,都存在使用不同的手势来控制进度、亮度\音量和暂停播放等功能。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() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //监听触摸事件
                return mGestureDetector.onTouchEvent(event);
            }
        });
        
        setOnGenericMotionListener(new OnGenericMotionListener() {
            @Override
            public boolean onGenericMotion(View v, MotionEvent event) {
                Log.d(TAG, "onGenericMotion: ");
                //监听鼠标右键点击事件
                return mGestureDetector.onGenericMotionEvent(event);
            }
        });
    }

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

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

GestureDetector源码分析

1. 初始化处理

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

    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }

    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
        //初始化Handler
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        //设置Listener
        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) {
            //相当于下面的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;
    }

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

  private class GestureHandler extends Handler {
        //省略构造函数...

        @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
                //这里控制SingleTapConfirmed的回调,
                if (mDoubleTapListener != null) {
                    if (!mStillDown) {
                        //如果已经松开,就立刻调用SingleTapConfirmed
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        //如果处理Message的时候还没松开,就设置mDeferConfirmSingleTap为true,在UP事件的时候调用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

            default:
                throw new RuntimeException("Unknown message " + msg); //never
            }
        }
    }

    //长按处理
    private void dispatchLongPress() {
        mHandler.removeMessages(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }

2. 输入处理

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

    public boolean onTouchEvent(MotionEvent ev) {
        //检查事件输入的一致性,log出来一致性的信息,如:有事件只有up没有down
        if (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;
    }

上面的注释写得很清楚了,主要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 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
                    //延时发出单击事件,如果到了时间(300ms)还没有取消的话就确认是TAP事件了
                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
                }
            }

            mDownFocusX = mLastFocusX = focusX;
            mDownFocusY = mLastFocusY = focusY;
            //重置mCurrentDownEvent
            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);
            }
            //延时发送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);
    }

可见,对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为true
                final 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;
    //...

可见,对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 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);
                }
            }
            //重置mPreviousUpEvent
            if (mPreviousUpEvent != null) {
                mPreviousUpEvent.recycle();
            }
            // Hold the event we obtained above - listeners may have changed the original.
            mPreviousUpEvent = currentUpEvent;
            //回收mVelocityTracker
            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;
    //...

可见,对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) {
                        //如果已经松开,就立刻调用SingleTapConfirmed
                        mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                    } else {
                        //如果处理Message的时候还没松开,就设置mDeferConfirmSingleTap为true,在UP事件的时候调用SingleTapConfirme
                        mDeferConfirmSingleTap = true;
                    }
                }
                break;

之前看过,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;
    }

这是对多个手指的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;
    }

由此可以,当按键按下(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的设计,和对触摸事件的处理方式是很值得我学习的,记录分享至此,水平有限,如果错漏,欢迎指正和讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,373评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,732评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,163评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,700评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,036评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,425评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,737评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,421评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,141评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,398评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,908评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,276评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,907评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,018评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,772评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,448评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,325评论 2 261

推荐阅读更多精彩内容