GestureDetector源码解析

1 简介

手势检测器GestureDetector用于帮助开发者辨别一些基本的触摸手势,如点击、长按、滑动等。这使得开发者可以专注于业务处理,不用再花精力去处理手势识别相关逻辑。

GestureDetector类包含三个监听接口OnGestureListener, OnDoubleTapListener, OnContextClickListener,一个外部类SimpleOnGestureListener,一个内部类GestureHandler。其中OnGestureListener会监听单击,滑动,长按,fling等,OnDoubleTapListener监听双击和单击事件,OnContextClickListener用于监听外接设备。 SimpleOnGestureListener是实现了OnGestureListener, OnDoubleTapListener, OnContextClickListener的空类,使用时,一般继承SimpleOnGestureListener就可以了。GestureDetector内部用GestureHandler来处理长按,单击等

2 接口介绍

public interface OnGestureListener {
    boolean onDown(MotionEvent e);
    void onShowPress(MotionEvent e);
    boolean onSingleTapUp(MotionEvent e);
    boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
    void onLongPress(MotionEvent e);
    boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
}
public interface OnDoubleTapListener {
    boolean onSingleTapConfirmed(MotionEvent e);
    boolean onDoubleTap(MotionEvent e);
    boolean onDoubleTapEvent(MotionEvent e);
}
public interface OnContextClickListener {
    boolean onContextClick(MotionEvent e);
}

3 源码分析

GestureDetector中的逻辑处理可以拆解成三部分,第一部分是在onTouchEvent方法中的逻辑处理和回调方法,第二部分是通过handler的回调方法,第三部分是控制变量相关的处理。 GestureDetector的核心逻辑是放在onTouchEvent中的,我们先来看下onTouchEvent的结构:

/**
 * Analyzes the given motion event and if applicable triggers the
 * appropriate callbacks on the {@link OnGestureListener} supplied.
 *
 * @param ev The current motion event.
 * @return true if the {@link OnGestureListener} consumed the event,
 *              else false.
 */
public boolean onTouchEvent(MotionEvent ev) {
    ...
    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;
    final boolean isGeneratedGesture =
            (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;

    // 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);
    }
    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:
            ...
        case MotionEvent.ACTION_POINTER_UP:
            ...
        case MotionEvent.ACTION_DOWN:
            ...
        case MotionEvent.ACTION_MOVE:
            ...
        case MotionEvent.ACTION_UP:
            ...
        case MotionEvent.ACTION_CANCEL:
            ...
    }
    ...
    return handled;
}

先是获取事件坐标(focusX, focusY),如果是多点触控的话,取的是所有触控点(排除抬起点ACTION_POINTER_UP)的平均值。接着进入到各个action处理中。先来看onTouchEvent中直接回调是怎么处理的:

3.1 onTouchEvent中的直接回调

ACTION_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
            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;
    mHasRecordedClassification = false;
    ...
    handled |= mListener.onDown(ev);
    break;

先掠过if (mDoubleTapListener != null)那一段的处理,直接看下面。

先是一堆属性值和控制变量的设置,注意mCurrentDownEvent,它被看作是双击事件中第一次点击时的DOWN事件,后面会说到。调用回调onDown()。

现在来看if (mDoubleTapListener != null)那一段,这一段是用来处理双击的逻辑。先判断是否已经发送过TAP消息,mCurrentDownEvent不为空,mPreviousUpEvent是第一次点击时的ACTION_UP事件,isConsideredDoubleTap()从字面意思上看,是判断两次点击是否可以算作一次双击行为。从源码上看,是通过两个条件来判断的,一是第一次点击的UP事件和第二次点击的DOWN事件之间的时间间隔要在DOUBLE_TAP_TIMEOUT和DOUBLE_TAP_MIN_TIME之间;二是第一次点击的DOWN事件点和第二次点击的DOWN事件点的距离要小于doubleTapSlop。如果以上条件都满足了,就认为这次DOWN事件是一次双击行为,会调用回调onDoubleTap()和onDoubleTapEvent()。否则的话,发送空消息TAP,认为是第一次点击行为。

注意:onDoubleTap传入的参数MotionEvent是第一次点击时的DOWN事件,且onDoubleTap只会在此处被调用一次;onDoubleTapEvent()传入的参数MotionEvent是第二次点击时的DOWN事件。确认是双击后,onDoubleTapEvent()会在MOVE事件和UP事件里都会被调用一次。

ACTION_MOVE

case MotionEvent.ACTION_MOVE:
    ...
    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) {
        final int deltaX = (int) (focusX - mDownFocusX);
        final int deltaY = (int) (focusY - mDownFocusY);
        int distance = (deltaX * deltaX) + (deltaY * deltaY);
        int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
        ...

        if (distance > slopSquare) {
            ...
            handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
            mLastFocusX = focusX;
            mLastFocusY = focusY;
            mAlwaysInTapRegion = false;
            ...
        }
        ...
    } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
        mLastFocusX = focusX;
        mLastFocusY = focusY;
    }
    ...
    break;

如果已经在DOWN事件中确认是双击(mIsDoubleTapping为true),会再次调用回调onDoubleTapEvent(ev),此时传入的MotionEvent,是第二次点击的MOVE事件。

否则,判断控制变量mAlwaysInTapRegion是否为true, 为true的话,如果滑动距离(deltaX * deltaX) + (deltaY * deltaY)超出slopSquare, 认为是scroll,调用回调onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY),其中e1是DOWN事件的Motionevent ,e2是MOVE事件的MotionEvent, distanceX和 distanceY是上次滑动点和本次滑动点的距离差。

如果mAlwaysInTapRegion是false,走if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1))分支,调用onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY),参数解释同上。

ACTION_UP

case MotionEvent.ACTION_UP:
    ...
    MotionEvent currentUpEvent = MotionEvent.obtain(ev);
    if (mIsDoubleTapping) {
        // Finally, give the up event of the double-tap
        ...
        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    } 
    ...
    else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
        ...
        handled = mListener.onSingleTapUp(ev);
        if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
            mDoubleTapListener.onSingleTapConfirmed(ev);
        }
    } else if (!mIgnoreNextUpEvent) {
        // 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);
        }
    }
    ...
    mIsDoubleTapping = false;
    mDeferConfirmSingleTap = false;
    mIgnoreNextUpEvent = false;
    break;

如果已经在DOWN事件中确认是双击(mIsDoubleTapping为true),会再次调用回调onDoubleTapEvent(ev),此时传入的MotionEvent时第二次点击的UP事件。

否则如果mAlwaysInTapRegion为true,调用回调onSingleTapUp(ev);此时如果mDeferConfirmSingleTap为ture,调用回调onSingleTapConfirmed(ev)。mIgnoreNextUpEvent一般是false,可忽略掉。

否则,判定是否是fling状态,获取velocityX, velocityY, 如果有一个超过给定的最小fling值,认为是fling, 调用回调onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); e1是本次Down事件的MotionEvent,e2是本次UP事件的MotionEvent, velocityX, velocityY是当前的x,y方向的速度值。

ACTION_POINTER_DOWN

case MotionEvent.ACTION_POINTER_DOWN:
    mDownFocusX = mLastFocusX = focusX;
    mDownFocusY = mLastFocusY = focusY;
    // Cancel long press and taps
    cancelTaps();
    break;

处理的是多点触控中的POINTER_DOWN事件,赋值mDownFocusX, mDownFocusY, mLastFocusX, mLastFocusY。cancelTaps()作用是remove掉所有的message和重置所有和tap相关的控制变量。

ACTION_POINTER_UP

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);


        final float dot = x + y;
        if (dot < 0) {
            mVelocityTracker.clear();
            break;
        }
    }
    break;

考虑的是多点触控模式下,手指抬起时的状态。重置mDownFocusX, mDownFocusY, mLastFocusX, mLastFocusY。

3.2 通过GestureHandler处理的回调

@Override
public void handleMessage(Message msg) {
    switch (msg.what) {
        case SHOW_PRESS:
            mListener.onShowPress(mCurrentDownEvent);
            break;
        case LONG_PRESS:
            recordGestureClassification(msg.arg1);
            dispatchLongPress();
            break;
        case TAP:
            // If the user's finger is still down, do not count it as a tap
            if (mDoubleTapListener != null) {
                if (!mStillDown) {
                    recordGestureClassification(
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
                    mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                } else {
                    mDeferConfirmSingleTap = true;
                }
            }
            break;
        default:
            throw new RuntimeException("Unknown message " + msg); //never
    }
}

从代码中看出来,handleMessage中关联的回调方法是onShowPress(),onLongPress(),onSingleTapConfirmed()。

先来看下消息发送和取消的时机。

Public boolean onTouchEvent(MotionEvent ev) {
    case MotionEvent.ACTION_POINTER_DOWN:
        //Cancel long press and taps
        cancelTaps();
    ...
    case MotionEvent.ACTION_DOWN:
        ...
        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;
            ...
        } else {
            // This is a first tap
            mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
        }
        ...
        mAlwaysInTapRegion = true;
        mStillDown = true;
        mInLongPress = false;
        ...
        if (mIsLongpressEnabled) {
            mHandler.removeMessages(LONG_PRESS);
            mHandler.sendMessageAtTime(
                    mHandler.obtainMessage(
                            LONG_PRESS,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
                            0 /* arg2 */),
                            mCurrentDownEvent.getDownTime()
                            + ViewConfiguration.getLongPressTimeout());
        }
        mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime()+TAP_TIMEOUT);

    case MotionEvent.ACTION_MOVE:
        if (mInLongPress || mInContextClick) {
            break;
        }
        if (mIsDoubleTapping) {
            ...
        } else if (mAlwaysInTapRegion) {
            final int deltaX = (int) (focusX - mDownFocusX);
            final int deltaY = (int) (focusY - mDownFocusY);
            int distance = (deltaX * deltaX) + (deltaY * deltaY);
            int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
            ...
            if (distance > slopSquare) {
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                mLastFocusX = focusX;
                mLastFocusY = focusY;
                mAlwaysInTapRegion = false;
                mHandler.removeMessages(TAP);
                mHandler.removeMessages(SHOW_PRESS);
                mHandler.removeMessages(LONG_PRESS);
            }
        }
    case MotionEvent.ACTION_UP:
        mStillDown = false;
        ...
        if (mIsDoubleTapping) {
            ...
        } else if (mInLongPress) {
            mHandler.removeMessages(TAP);
            mInLongPress = false;
        }
        mHandler.removeMessages(SHOW_PRESS);
        mHandler.removeMessages(LONG_PRESS);
    case MotionEvent.ACTION_CANCEL:
        cancel();
}
private void dispatchLongPress() {
    mHandler.removeMessages(TAP);
    mDeferConfirmSingleTap = false;
    mInLongPress = true;
    mListener.onLongPress(mCurrentDownEvent);
}
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;
}
private void cancel() {
    mHandler.removeMessages(SHOW_PRESS);
    mHandler.removeMessages(LONG_PRESS);
    mHandler.removeMessages(TAP);
    mVelocityTracker.recycle();
    mVelocityTracker = null;
    mIsDoubleTapping = false;
    mStillDown = false;
    mAlwaysInTapRegion = false;
    mAlwaysInBiggerTapRegion = false;
    mDeferConfirmSingleTap = false;
    mInLongPress = false;
    mInContextClick = false;
    mIgnoreNextUpEvent = false;
}

SHOW_PRESS

SHOW_PRESS的发送时机是DOWN事件发生时,延时TAP_TIMEOUT发送;UP,CANCEL,ACTION_POINTER_DOWN事件发生就remove掉该消息;此外,在MOVE事件中,如果mAlwaysInTapRegion为true的情况下,调用了onScroll(),也会remove掉该消息。 handleMessage收到该消息,会直接调用onShowPress()

LONG_PRESS

顾名思义,长按,发送的时机是DOWN事件里,如果mIsLongpressEnabled为true,即支持长按,延时ViewConfiguration.getLongPressTimeout()发送;remove时机同SHOW_PRESS。

handleMessage收到该消息,会调用方法dispatchLongPress(),remove掉TAP消息,设置控制变量mDeferConfirmSingleTap和mInLongPress,调用回调onLongPress(ev)。

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

TAP

在DOWN事件中,判断不是双击,会延时DOUBLE_TAP_TIMEOUT发送该消息。 MOVE,CANCEL,ACTION_POINTER_DOWN事件中的remove行为同SHOW_PRESS,UP事件中,mInLongPress为true的情况下remove掉,如果确认是长按事件了,也会remove掉该消息。

注意,handleMessage中的回调,参数传入的都是DOWN事件的MotionEvent.

3.3 控制变量

GestureDetector的控制变量主要是以下这几个

private boolean mStillDown;
private boolean mDeferConfirmSingleTap;
private boolean mInLongPress;

private boolean mAlwaysInTapRegion;
private boolean mAlwaysInBiggerTapRegion;

private boolean mIsDoubleTapping;

private boolean mIsLongpressEnabled;

mStillDown的作用是在handleMessage中处理TAP时,控制是否调用回调onSingleTapConfirmed(),只有在mStillDown是false时才可以调用。DOWN事件中它被赋值true,UP和CANCEL事件中赋值false。

mDeferConfirmSingleTap为true表示可以在UP事件中调用onSingleTapConfirmed(ev),而mDeferConfirmSingleTap默认是false,只有在处理TAP消息时,mStillDown为true时,才被赋值为true。

mInLongPress默认为false,只有在处理LONG_PRESS消息时,设为true。为true的时候,不执行MOVE事件。

mAlwaysInTapRegion在DOWN事件赋值为true,ACTION_POINTER_DOWN和CANCEL事件设为false。MOVE事件中,在它为true的条件下调用了onScroll()后,会赋值为false。UP事件中,如果已经是false,说明这次行为是scroll,不是的话,说明是单击,会调用onSingleTapUp()和onSingleTapConfirmed()。综上,mAlwaysInTapRegion是用来判断滑动事件中的第一次滑动的。

mAlwaysInBiggerTapRegion用于判断是否需要执行isConsideredDoubleTap()方法,这应该是一个优化,DOWN事件中设为true,MOVE事件中如果是scroll行为,设为false,即确认不是双击,不用再走一遍isConsideredDoubleTap()了。

mIsDoubleTapping标志是否双击,DOWN事件时确定是双击后,设为true; UP事件时重置,设为false。ACTION_POINTER_DOWN和CANCEL事件同样重置,设为false。它主要用来判断要不要在MOVE事件和UP事件时,调用onDoubleTapEvent()。

mIsLongpressEnabled默认是true,即默认是支持长按的,你也可以通过setIsLongpressEnabled()禁用长按。它的作用是在DOWN事件中控制是否要发送LONG_PRESS消息。

总结

通过上面的源码拆解分析,可以看出来GestureDetector如何通过控制变量,handler和MotionEvent共同来判断触摸行为的。

DOWN事件必然会调用的回调是onDown(),确认是双击后,可能会调用的是onDoubleTap()和onDoubleTapEvent(),之后MOVE和UP事件会各调用一次onDoubleTapEvent()。

MOVE事件先判断是否是长按(mInLongPress),再判断是否是双击(onDoubleTapEvent),接着判断是否是滑动(onScroll)

UP事件先判断是否是双击(mIsDoubleTapping,onDoubleTapEvent()),再判断是否是长按(mInLongPress),接着判断是否是单击(mAlwaysInTapRegion,onSingleTapUp,onSingleTapConfirmed),最后再判断是不是fling(onFling)。

猜你喜欢

转载自juejin.im/post/7021082382375600142