Android知识Android进阶之路Android开发

Android GestureDetector 源码分析

2018-03-23  本文已影响0人  OhHoo

前言

工作这段时间以来,陆陆续续看了一些类的源码,但是都没有详细的记录下来,只是在笔记上记录了一些功能性的代码,导致一段时间之后,完完全全忘记了。所以今天打算回顾梳理系统里的手势判断类 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!


源码分析

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 事件总结:

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 事件总结:

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 事件总结:

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事件处理,其实就是做一些取消操作而让多点触摸不影响单点触摸的应用,例如在多个手指落下的时候取消点击信息等。

后记

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

上一篇 下一篇

猜你喜欢

热点阅读