美文网首页Android开发干货集中营涛锅锅的Android资料Android
Android手势检测——GestureDetector全面分析

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

作者: 炎之铠 | 来源:发表于2017-09-07 15:40 被阅读489次

    出处
    炎之铠邮箱: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的设计,和对触摸事件的处理方式是很值得我学习的,记录分享至此,水平有限,如果错漏,欢迎指正和讨论。

    相关文章

      网友评论

      • d245956d0304:博主我想把你的文章分享到我的公众号可以么。

      本文标题:Android手势检测——GestureDetector全面分析

      本文链接:https://www.haomeiwen.com/subject/gspijxtx.html