GestureDetector event source code analysis

GestureDetector event source code analysis

1. What is GestureDetector?

Gesture: Gesture

Detector: Detector.

For gestures in MotionEventup , there are only simple gestures such as , down, moveand so on. When the gestures are complicated, these simple actions obviously cannot satisfy us, or it will be more complicated for us to realize it ourselves.

GestureDetector is an official class provided to us to detect gestures. It helps us encapsulate a lot of gesture events internally and provides us with three interfaces:

  • OnContextClickListener is used to monitor mouse click events

    • onContextClick Detect mouse click

  • OnGestureListener is used to monitor various gestures

    • onDown when the finger is pressed

    • onShowPress User did downbut didn't upandmove

    • onSingleTapUp Click event (up soon after being pressed)

    • onScroll finger slides across the screen

    • onLongPress long press operation

    • onFling Swipe your finger across the screen

  • OnDoubleTapListener is used to listen to double-click events

    • onSingleTapConfirmed Click event (different from the above click event, mainly to detect non-double-click events, that is, there is no second press after the first press)

    • onDoubleTap Double click event, triggered on second press

    • onDoubleTapEvent events after a double click, including down, move andup

At the same time, it also provides a static class SimpleOnGestureListener , which inherits the above three interfaces. When we need to implement many gestures, we can choose to inherit this class.

This article is mainly aimed at the source code analysis of how GestureDetector determines these events, without explaining how to use it.

二、OnGestureListener

1.onDown

This is the simplest gesture, and it will be triggered when the finger is pressed.

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

The return value here handledis used to determine whether the event has been processed. For details, refer to the return value of () in event distribution onTouch.

mListenerIt is the listener OnGestureListener . So it will be executed after the finger is pressed onDown().

2.onShowPress

This gesture means that the user does not move or lift after pressing it. Let's take a look at the implementation of the source code:

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

Here we can see that when the finger is pressed, GestureDetector will call the internal handler to send a SHOW_PRESSmessage. Then look at this inner handlerclass

    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;
                ...
            }
        }
    }

In this class, if the received information is SHOW_PRESSyes, then the method of mListeneronShowPress() will be called . The parameters passed in here mCurrentDownEventrepresent the current action.

So onDownwhat is the difference between him and me?

First, pay attention to the line of code that sends the message

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

There is a set time for sending a message here, TAP_TIMEOUTand the value is 300ms, which means that the message will be sent after a delay of 300ms. So it won't trigger immediately after finger press onShowPress.

Secondly, we can know from the official comment that there is no sum onShowPressafter the finger is pressed , so if the sum occurs within 300ms , the message should be cancelled. This is indeed the case in the source codemoveupmoveup

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

In ACTION_MOVE, if the distance moved by the finger exceeds a slopSquarethreshold, SHOW_PRESSthe be canceled. ACTION_UPis clearer.

So ShowPressit is expressed that the finger is pressed for more than 300ms, and there is no movement beyond the threshold or ACTION_UP event within 300ms

But watch out! It is not a long press, it onLongPressis different from a long press, and we will talk about it when we analyze it below.

3.onLongPress

This is the gesture that handles the long press event. The triggering method onShowPressis similar, and the message is also sent through mHandler . Let's look at the processing of the message first.

 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()This function is called here

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

This function is very simple and all are posted here. First, the message of the TAP click event is removed, and then two flags are set. Finally called mListener.onLongPress.

Now let's analyze how to realize the judgment of long press.

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

mIsLongpressEnabledIs a global variable, we can setIsLongpressEnabled()set whether to enable long press by calling.

The same is to delay sending messages, but the time here is 500ms, which is 200ms later than onShowPress . There should ACTION_MOVEalso ACTION_UPbe a cancellation of the long press message.

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 */));
        }
     
}

In ACTION_MOVE, there are three LONG_PRESSplaces for message processing.

The first place is for ambiguous events. For this parameter, it means that the user's intention for the current event flow has not yet been determined. And the following operation is to re-initiate a message. Why do you do that? The official comment is thisMotionEvent.CLASSIFICATION_AMBIGUOUS_GESTUREonLongPress

// 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.

What this means here is that when the slope of the touch changes, there is a risk of not responding to user input. So here you need to rearrange the long press message.

I don't know exactly what kind of situation it is, but I guess it has something to do with the contact surface that the finger presses on the screen.

The second place is relatively simple. When the finger moves beyond the threshold, the long press event is canceled, which is easier to understand here.

In the third place, the official explanation for MotionEvent.CLASSIFICATION_DEEP_PRESS is as follows:

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.

Indicates that the user presses the screen harder to accelerate the long press behavior.

So we can see that the third place will send the Long_Press message directly.

ACTION_UPThe processing in is the onShowPresssame as in

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

So far we can get a difference between onShowPressandonLongPress :

  • onShowPressTriggered 300ms after the finger is pressed, onLongPressand 500ms after the finger is pressed.

4.onSingleTapUp

This gesture means that the finger is pressed and then lifted, indicating a click event up.

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

In ACTION_UPthe processing, we can see that if the lift is double-clicked or lifted after a long press, it will not be triggered onSingleTapUp. And mAlwaysInTapRegionthis parameter will ACTION_MOVEbe set to false in , which means that if the finger moves, it will not trigger onSingleTapUp.

From here we can also see another difference between onShowPressand :onLongPress

  • onShowPressTriggered when liftedonSingleTapUp

  • onLongPressWill not trigger after liftingonSingleTapUp

5.onScroll

This gesture represents moving a finger across the screen.

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

We have seen the first call before. When the finger moves beyond a threshold, it means that it starts to move, and it will be called .mListener.onScroll

And the second place is that the finger is already in the process of moving, and it will be called if >=1 in a certain direction of movement.

In fact Scroll, it ACTION_MOVEis almost the same, it is caused by finger movement, but Scroll needs to move beyond the threshold to trigger.

6.onFling

This gesture represents a quick swipe of the finger across the screen.

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

First of all, if the current lifting is not a double-click lifting, long-pressed and lifted, or still lifted at the pressed position, it will enter the process onFling.

Internally, the VelocityTracker class is used to calculate the speed of finger movement. If the speed of the finger moving in a certain direction exceeds the set threshold, then onFlingthe event will be triggered.

summary

This section mainly analyzes how the six gesture events defined in OnGestureListener are triggered.

  1. onDownFired after a finger press.

  2. onShowPressTriggered 300ms after the finger is pressed. If the finger movement exceeds the threshold or is lifted during the period, it will be cancelled.

  3. onLongPressTriggered 500ms after the finger is pressed. During this period, if the finger moves beyond the threshold or is lifted, it will be canceled.

  4. onSingleTapUpIt will be triggered when it is pressed and then lifted, and there is no movement beyond the threshold, no long press, no double click.

  5. onScroll Triggered when a finger moves across the screen beyond a threshold.

  6. onFlingTriggered when the finger moves faster than the threshold on the screen .

三、OnDoubleTapListener

Analyzing the three gestures of this interface is not as separate as before, and it will be clearer to analyze them together.

@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;
}

mStillDownIs a global variable used to determine whether the finger is still on the screen. Set to true in ACTION_DOWN, ACTION_UPset to false in .

Then the logic is very simple. If the finger has left the screen when receiving the message, it will be called onSingleTapConfirmed; if it is still on the screen, it will be mDeferConfirmSingleTapset to 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);
            }
       }
}

If it is the first click, ACTION_DOWNthe TAP message will be sent with a delay, where the delay is 300ms.

If it is the second click, the previously delayed TAPmessage will be canceled first, and then onDoubleTap()and 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);
        }    
}

In ACTION_MOVE, if the double-click event is triggered, it will also be called 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);
            }
        }
}

There are two situations here ACTION_UP. One is that the double-click has been triggered, then it will be called onDoubleTapEvent()to handle upthe event.

In the second case, the double click is not triggered. That is to say, after the first click is pressed, it is lifted after 300ms. Then onSingleTapConfirmed()the event will also be processed at this time. Note that if a long press is triggered, you will not go to this branch .

summary

DoubleTapListener has only three events, and the principle of triggering is relatively simple.

  1. onSingleTapConfirmed This event is triggered when the screen is tapped only once. That is, this event will be triggered when the finger is pressed and lifted within 500ms.

  2. onDoubleTap Fires on the second press of a double tap.

  3. onDoubleTapEvent After double-clicking ACTION_DOWN , ACTION_MOVE , ACTION_UPwill be triggered. Using this feature, you can do a lot of custom operations with double-click.

Four. Summary

This article analyzes the triggering principles of various events in OnGestureListener and OnDoubleTapListener . For OnContextClickListener , because it is not used much, it will not be analyzed here.

Although the two interfaces define various gestures for us, they also have certain limitations. For example, onSingleTapUp will not be triggered after a long press , so if we want to handle the lift event after a long press, we still need to ACTION_UPhandle it ourselves.

I think the best experience for gestures requires the use of View.OnTouchListener and GestureDetector together to more flexibly realize our needs.

This source code analysis still learned a lot, let me summarize the points that I think are very useful:

  1. For the judgment of the long press event, you can delay sending a message through the message queue. In ACTION_MOVEand ACTION_UPin to cancel the message.

  2. For the double-click event, you can customize the down, move, upevent after the double-click. Specific functions can be set by double-clicking.

  3. VelocityTracker can be used to obtain the speed of finger movement.

If you find an error, please point it out! Will fix it in time!

Guess you like

Origin blog.csdn.net/qq_43478882/article/details/120273045