Android GestureDetector 源码分析

作者: OhHoo | 来源:发表于2018-03-23 13:56 被阅读0次

    前言

    工作这段时间以来,陆陆续续看了一些类的源码,但是都没有详细的记录下来,只是在笔记上记录了一些功能性的代码,导致一段时间之后,完完全全忘记了。所以今天打算回顾梳理系统里的手势判断类 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事件处理,其实就是做一些取消操作而让多点触摸不影响单点触摸的应用,例如在多个手指落下的时候取消点击信息等。

    后记

    第一次分析源码,如果有错误或者需要补充的,欢迎留言讨论。

    相关文章

      网友评论

        本文标题:Android GestureDetector 源码分析

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