前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GestureDetector源码解析

GestureDetector源码解析

原创
作者头像
好好学习吧
修改2021-10-18 10:24:28
4560
修改2021-10-18 10:24:28
举报
文章被收录于专栏:今天学习了吗今天学习了吗

1 简介

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

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

2 接口介绍

代码语言:javascript
复制
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); 
} 
代码语言:javascript
复制
public interface OnDoubleTapListener { 
    boolean onSingleTapConfirmed(MotionEvent e); 
    boolean onDoubleTap(MotionEvent e); 
    boolean onDoubleTapEvent(MotionEvent e); 
} 
代码语言:javascript
复制
public interface OnContextClickListener { 
    boolean onContextClick(MotionEvent e); 
} 

3 源码分析

    GestureDetector中的逻辑处理可以拆解成三部分,第一部分是在onTouchEvent中的逻辑处理和回调方法,第二部分是通过handler的回调方法,第三部分是控制变量相关的处理。

    GestureDetector的核心逻辑是放在onTouchEvent中的,我们先来看下onTouchEvent的结构:

代码语言:javascript
复制
/** 
* 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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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处理的回调

先来看下handleMessage的处理逻辑

代码语言:javascript
复制
@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()。

    先来看下三种message的发送和取消的时机。

代码语言:javascript
复制
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(); 
} 
代码语言:javascript
复制
private void dispatchLongPress() { 
    mHandler.removeMessages(TAP); 
    mDeferConfirmSingleTap = false; 
    mInLongPress = true; 
    mListener.onLongPress(mCurrentDownEvent); 
} 
代码语言:javascript
复制
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; 
} 
代码语言:javascript
复制
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)。

代码语言:javascript
复制
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的控制变量主要是以下这几个

代码语言:javascript
复制
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()。

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档