GestureDetector事件源码分析

GestureDetector事件源码分析

一、什么是GestureDetector?

Gesture :手势

Detector : 检测器。

对于MotionEvent里的手势,只有简单的updownmove等简单手势。当手势复杂的时候,这些简单的动作显然满足不了我们,或者说我们自己实现会比较复杂。

GestureDetector是官方提供给我们用于检测手势的类,内部帮我们封装好了很多的手势事件,并且提供给了我们三个接口:

  • OnContextClickListener 用于监听鼠标的点击事件

    • onContextClick 检测鼠标单击

  • OnGestureListener 用于监听各种手势

    • onDown 当手指按下

    • onShowPress 用户执行了down但是未执行upmove

    • onSingleTapUp 单击事件 (按下后很快抬起)

    • onScroll 手指在屏幕上滑动

    • onLongPress 长按操作

    • onFling 手指在屏幕上快速滑动

  • OnDoubleTapListener 用于监听双击事件

    • onSingleTapConfirmed 单击事件 (与上面的单击事件不同,主要是为了检测非双击事件,也就是第一次按下后没有按下第二次)

    • onDoubleTap 双击事件,在第二次按下时触发

    • onDoubleTapEvent 双击后的事件,包括down, move up

同时也提供了一个静态类SimpleOnGestureListener,它继承了上面的三个接口,当我们需要实现的手势很多时就可以选择继承这个类。

本篇文章主要是针对GestureDetector是如何判定这些事件做的源码分析,不做使用的讲解。

二、OnGestureListener

1.onDown

这是最简单的手势了,手指按下就会触发。

public boolean onTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;    
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            ...
            handled |= mListener.onDown(ev);
            break;
        ...
    }
    return handled;
}

这里的返回值handled用于判断本次事件是否处理完毕。具体可参照事件分发中的onTouch()返回值。

mListener就是OnGestureListener 这个监听器了。所以手指按下之后就会执行onDown().

2.onShowPress

这个手势表示用户按下后没有移动也没有抬起,我们看一下源码的实现:

public boolean onTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;    
    switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            ...
            mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
                        mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
            break;
        ...
    }
    return handled;
}

这里我们可以看到,在手指按下的时候,GestureDetector就会调用内部的handler发送一个SHOW_PRESS的消息。那看一下这个内部的handler

    private class GestureHandler extends Handler {
        GestureHandler() {
            super();
        }
​
        GestureHandler(Handler handler) {
            super(handler.getLooper());
        }
​
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SHOW_PRESS:
                    mListener.onShowPress(mCurrentDownEvent);
                    break;
                ...
            }
        }
    }

在这个类里,收到的信息如果是SHOW_PRESS的话,那么就会调用mListeneronShowPress()方法了。而这里传入的参数mCurrentDownEvent表示当前的动作。

那他和onDown 有什么区别呢?

首先注意一下发送消息的这一行代码

mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
   mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);

这里发送消息是有设定一个时间的,TAP_TIMEOUT的值是300ms,也就是相当于在延时300ms后才会发送这条消息。所以手指按下后不会立即触发onShowPress

其次通过官方的注释可以得知,onShowPress表示手指按下后没有moveup,那么如果在300ms之内发生了moveup,这次消息应该会被取消的吧。到源码里看确实是这样

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_MOVE:
        if (mIsDoubleTapping) {
            ...
        } else if (mAlwaysInTapRegion) {
            final int deltaX = (int) (focusX - mDownFocusX);
            final int deltaY = (int) (focusY - mDownFocusY);
            int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
            int distance = (deltaX * deltaX) + (deltaY * deltaY);
            ...
            if (distance > slopSquare) {
                ...
                mHandler.removeMessages(SHOW_PRESS);
                ...
            }
        }
        ...
        break;
        
     case MotionEvent.ACTION_UP: 
        ...
        mHandler.removeMessages(SHOW_PRESS);
        ...
        break;
}

ACTION_MOVE里,如果手指移动的距离超过了一个slopSquare阈值,那么就会把SHOW_PRESS这条消息给取消。ACTION_UP里就更清晰了。

所以ShowPress表达的是手指按下了超过300ms,且300ms内没有移动超过阈值或有ACTION_UP事件

但是注意!它不是长按,它和onLongPress是有区别的,下面我们分析的时候会讲到。

3.onLongPress

这是处理长按事件的手势。触发的方式和onShowPress差不多,也是通过mHandler发送消息。我们先看一下消息的处理。

 private class GestureHandler extends Handler {
        GestureHandler() {
            super();
        }
​
        GestureHandler(Handler handler) {
            super(handler.getLooper());
        }
​
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ...
                case LONG_PRESS:
                    ...
                    dispatchLongPress();
                    break;
                ...
            }
        }
    }

这里调用了dispatchLongPress()这个函数

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

这个函数很简单这里就全部贴出来了。先是移除了TAP单击事件的消息,然后设置了两个标志位。最后调用了mListener.onLongPress

那现在就要分析一下怎么实现长按的判断的。

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
        if (mIsLongpressEnabled) {
            mHandler.removeMessages(LONG_PRESS);
            mHandler.sendMessageAtTime(
                    mHandler.obtainMessage(
                            LONG_PRESS,
                            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
                            0 /* arg2 */),
                    mCurrentDownEvent.getDownTime()
                            + ViewConfiguration.getLongPressTimeout());
        }
        break;
}

mIsLongpressEnabled是一个全局变量,我们可以通过调用setIsLongpressEnabled()来设置是否开启长按可用。

同样是延迟发送消息,只不过这里的时间是500ms,比onShowPress要晚200ms。那ACTION_MOVEACTION_UP里应该也会有长按消息的取消。

switch (action & MotionEvent.ACTION_MASK) {
    ...
    case MotionEvent.ACTION_MOVE:
        final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
        if (mIsDoubleTapping) { 
            ...
        } else if (mAlwaysInTapRegion) {
            final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
            final boolean shouldInhibitDefaultAction =
                    hasPendingLongPress && ambiguousGesture;
            if (shouldInhibitDefaultAction) {
                if (distance > slopSquare) {
                    mHandler.removeMessages(LONG_PRESS);
                    final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
                    mHandler.sendMessageAtTime(
                             mHandler.obtainMessage(
                                    LONG_PRESS,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
                                    0 /* arg2 */),
                            ev.getDownTime() + (long) (longPressTimeout * multiplier));
                }
                
            if (distance > slopSquare) {
                ...
                mHandler.removeMessages(LONG_PRESS);
           }
        }
            
        final boolean deepPress =
                motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
        if (deepPress && hasPendingLongPress) {
            mHandler.removeMessages(LONG_PRESS);
            mHandler.sendMessage(
                    mHandler.obtainMessage(
                          LONG_PRESS,
                          TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS,
                          0 /* arg2 */));
        }
     
}

ACTION_MOVE里,一共有三个对LONG_PRESS消息处理的地方。

第一个地方,是对于存在歧义的事件。对于MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE这个参数,表示用户对当前事件流的意图尚未确定。而下面的操作是重新发起了一次onLongPress的消息。为什么要这么做呢?官方的注释是这样的

// The default action here is to remove long press. But if the touch
// slop below gets increased, and we never exceed the modified touch
// slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
// will happen in response to user input. To prevent this,
// reschedule long press with a modified timeout.

这里的意思是当触摸的斜率改变,会有没有响应用户输入的风险。所以这里需要重新安排长按的消息。

对于具体是什么样的情况还不太明白,但是我估计是和手指按在屏幕上的接触面有关。

第二个地方就比较简单了,当手指移动超过阈值,就取消长按事件,这里比较好理解。

而第三个地方,对于MotionEvent.CLASSIFICATION_DEEP_PRESS官方是这样解释的:

Classification constant: Deep press. The current event stream represents the user intentionally pressing harder on the screen. This classification type should be used to accelerate the long press behaviour.

表示用户加大力度按压屏幕,用于加速长按行为。

所以我们可用看到第三个地方会直接发送Long_Press消息。

ACTION_UP里的处理就和onShowPress一样了

switch (action & MotionEvent.ACTION_MASK) {
    ...
    case MotionEvent.ACTION_UP:
        ...
        mHandler.removeMessages(LONG_PRESS);
        break;
     
}

至此我们可以得到onShowPressonLongPress的一个区别

  • onShowPress在手指按下300ms后触发,onLongPress在手指按下500ms后触发。

4.onSingleTapUp

这个手势表示手指按下后抬起,表示单击事件的up

switch (action & MotionEvent.ACTION_MASK) {  
     case MotionEvent.ACTION_UP:
         if (mIsDoubleTapping) {
             ...
         } else if (mInLongPress) {
             ...
         } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
             ...
             handled = mListener.onSingleTapUp(ev);
             ...
         }
}

ACTION_UP的处理里我们可以看到,如果这次抬起是双击后抬起,或者是长按后的抬起,都不会触发onSingleTapUp。而mAlwaysInTapRegion这个参数会在ACTION_MOVE里被置为false,也就是说手指如果移动了也不会触发onSingleTapUp

从这里我们也可以看出onShowPressonLongPress的另一个区别了:

  • onShowPress抬起后会触发onSingleTapUp

  • onLongPress抬起后不会触发onSingleTapUp

5.onScroll

这个手势表示手指在屏幕上移动。

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_MOVE:
        if (mIsDoubleTapping) {
            ...
        } else if (mAlwaysInTapRegion) {
            final int deltaX = (int) (focusX - mDownFocusX);
            final int deltaY = (int) (focusY - mDownFocusY);
            int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
            int distance = (deltaX * deltaX) + (deltaY * deltaY);
            ...
            if (distance > slopSquare) {
                ...
                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                ...
            }
        } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
            ...
            handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
            mLastFocusX = focusX;
            mLastFocusY = focusY;
        }
        ...
        break;
}

第一个调用的地方前面我们就见过,当手指移动超过了一个阈值就表示开始移动了,就会调用mListener.onScroll

而第二个地方呢就是手指已经在移动过程中了,移动的某一方向上>=1也会调用。

其实ScrollACTION_MOVE是差不多的,都是手指移动造成的,不过Scroll需要移动超过阈值才会触发。

6.onFling

这个手势表示手指在屏幕上快速滑动。

switch (action & MotionEvent.ACTION_MASK) {
    ...
     if (mIsDoubleTapping) {
         ...
     } else if (mInLongPress) {
         ...
     } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
         ...
     } 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);
         }
     }
     
}

首先如果本次抬起不属于双击的抬起、长按后抬起、还在按下的位置抬起,才会进入处理onFling.

而内部主要是借助VelocityTracker这个类来计算手指移动的速度。如果手指在某一方向上移动的速度超过了设定的阈值,那么就会触发onFling事件。

小结

本小节主要分析了OnGestureListener中定义的六个手势事件是如何触发的。

  1. onDown在手指按下后触发。

  2. onShowPress在手指按下300ms后触发。如果期间手指移动超过了阈值或抬起,会被取消。

  3. onLongPress在手指按下500ms后触发。期间如果手指移动超过了阈值或抬起,会被取消。

  4. onSingleTapUp按下后抬起,且期间没有移动超过阈值,没有长按,没有双击才会触发。

  5. onScroll 手指在屏幕上移动超过阈值会触发。

  6. onFling手指在屏幕上移动速度超过阈值会触发。

三、OnDoubleTapListener

分析这个接口的三个手势就不像前面分开分析了,一起分析会更清晰一点。

@Override
public void handleMessage(Message msg) {
    switch (msg.what) 
        ...
        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;
}

mStillDown是一个全局变量用来判断手指是否还在屏幕上。在ACTION_DOWN里设为true,在ACTION_UP里设为false。

那么逻辑就很简单了,收到消息时如果手指已经离开了屏幕,就调用onSingleTapConfirmed;如果还在屏幕上,就把mDeferConfirmSingleTap设为true.

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_DOWN:
        if (mDoubleTapListener != null) {
            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);
            }
       }
}

如果是第一次点击,那么就会在ACTION_DOWN里延时发送TAP消息,这里的延时是300ms。

如果是第二次点击,会先取消之前延时的TAP消息,然后会调用 onDoubleTap()onDoubleTapEvent()

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_MOVE:
        ...
        if (mIsDoubleTapping) {
            // Give the move events of the double-tap
            recordGestureClassification(
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
            handled |= mDoubleTapListener.onDoubleTapEvent(ev);
        }    
}

ACTION_MOVE里,如果是触发了双击事件,那么也会调用onDoubleTapEvent().

switch (action & MotionEvent.ACTION_MASK) {
    case MotionEvent.ACTION_UP:
        ...
        if (mIsDoubleTapping) {
            // Finally, give the up event of the double-tap
            handled |= mDoubleTapListener.onDoubleTapEvent(ev);
        } else if (mInLongPress) {
            ...
        } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
            ...
            if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                mDoubleTapListener.onSingleTapConfirmed(ev);
            }
        }
}

ACTION_UP里有两种情况。一种是已经触发了双击,那么就会调用onDoubleTapEvent()去处理up事件。

而第二种情况是没有触发双击。也就是说在第一次点击按下后,在300ms之后才抬起。那这时候也会处理onSingleTapConfirmed()事件。注意如果触发了长按就不会走到这个分支了

小结

DoubleTapListener只有三个事件,触发的原理也比较简单。

  1. onSingleTapConfirmed 只点击了一次屏幕,就会触发该事件。也就是在手指按下500ms内抬起就会触发这个事件。

  2. onDoubleTap 在双击的第二次按下时会触发。

  3. onDoubleTapEvent 在双击后的ACTION_DOWN , ACTION_MOVE ,ACTION_UP都会触发。利用这一特点就可以用双击做很多自定义操作。

四、总结

本文分析了OnGestureListenerOnDoubleTapListener中各种事件的触发原理。对于OnContextClickListener因为用的不多这里就不分析了。

虽然说两个接口为我们定义了各种各样的手势,但是也是有一定的局限性的。比如onSingleTapUp长按后不会触发,那么如果我们想处理长按后的抬起事件还是需要到ACTION_UP里自行处理。

对于手势的最佳体验我觉得是需要View.OnTouchListenerGestureDetector搭配着使用,才能更加灵活地实现我们的需求。

本次的源码分析还是学习到了不少东西,总结一下我觉得很有用的点:

  1. 对于长按事件的判断,可以通过消息队列,延迟发送一个消息。在ACTION_MOVEACTION_UP里去取消消息。

  2. 对于双击事件,可以自定义双击后的down, move, up事件。可以通过双击来设定特定的功能。

  3. VelocityTracker可以用来获取手指移动的速度。

如果发现错误,恳请指出!会及时修正!

猜你喜欢

转载自blog.csdn.net/qq_43478882/article/details/120273045