public final class

GestureDetectorCompat

extends java.lang.Object

 java.lang.Object

↳androidx.core.view.GestureDetectorCompat

Gradle dependencies

compile group: 'androidx.core', name: 'core', version: '1.9.0-alpha04'

  • groupId: androidx.core
  • artifactId: core
  • version: 1.9.0-alpha04

Artifact androidx.core:core:1.9.0-alpha04 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.core:core com.android.support:support-compat

Androidx class mapping:

androidx.core.view.GestureDetectorCompat android.support.v4.view.GestureDetectorCompat

Overview

Detects various gestures and events using the supplied MotionEvents. The callback will notify users when a particular motion event has occurred. This class should only be used with MotionEvents reported via touch (don't use for trackball events).

This compatibility implementation of the framework's GestureDetector guarantees the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.

To use this class:

Summary

Constructors
publicGestureDetectorCompat(Context context, OnGestureListener listener)

Creates a GestureDetectorCompat with the supplied listener.

publicGestureDetectorCompat(Context context, OnGestureListener listener, Handler handler)

Creates a GestureDetectorCompat with the supplied listener.

Methods
public booleanisLongpressEnabled()

public booleanonTouchEvent(MotionEvent event)

Analyzes the given motion event and if applicable triggers the appropriate callbacks on the supplied.

public voidsetIsLongpressEnabled(boolean enabled)

Set whether longpress is enabled, if this is enabled when a user presses and holds down you get a longpress event and nothing further.

public voidsetOnDoubleTapListener(OnDoubleTapListener listener)

Sets the listener which will be called for double-tap and related gestures.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public GestureDetectorCompat(Context context, OnGestureListener listener)

Creates a GestureDetectorCompat with the supplied listener. As usual, you may only use this constructor from a UI thread.

Parameters:

context: the application's context
listener: the listener invoked for all the callbacks, this must not be null.

See also: android.os.Handler

public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler)

Creates a GestureDetectorCompat with the supplied listener. As usual, you may only use this constructor from a UI thread.

Parameters:

context: the application's context
listener: the listener invoked for all the callbacks, this must not be null.
handler: the handler that will be used for posting deferred messages

See also: android.os.Handler

Methods

public boolean isLongpressEnabled()

Returns:

true if longpress is enabled, else false.

public boolean onTouchEvent(MotionEvent event)

Analyzes the given motion event and if applicable triggers the appropriate callbacks on the supplied.

Parameters:

event: The current motion event.

Returns:

true if the consumed the event, else false.

public void setIsLongpressEnabled(boolean enabled)

Set whether longpress is enabled, if this is enabled when a user presses and holds down you get a longpress event and nothing further. If it's disabled the user can press and hold down and then later moved their finger and you will get scroll events. By default longpress is enabled.

Parameters:

enabled: whether longpress should be enabled.

public void setOnDoubleTapListener(OnDoubleTapListener listener)

Sets the listener which will be called for double-tap and related gestures.

Parameters:

listener: the listener invoked for all the callbacks, or null to stop listening for double-tap gestures.

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.core.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Detects various gestures and events using the supplied {@link MotionEvent}s.
 * The {@link OnGestureListener} callback will notify users when a particular
 * motion event has occurred. This class should only be used with {@link MotionEvent}s
 * reported via touch (don't use for trackball events).
 *
 * <p>This compatibility implementation of the framework's GestureDetector guarantees
 * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.</p>
 *
 * To use this class:
 * <ul>
 *  <li>Create an instance of the {@code GestureDetectorCompat} for your {@link View}
 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
 *          will be executed when the events occur.
 * </ul>
 */
public final class GestureDetectorCompat {
    interface GestureDetectorCompatImpl {
        boolean isLongpressEnabled();
        boolean onTouchEvent(MotionEvent ev);
        void setIsLongpressEnabled(boolean enabled);
        void setOnDoubleTapListener(OnDoubleTapListener listener);
    }

    static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl {
        private int mTouchSlopSquare;
        private int mDoubleTapSlopSquare;
        private int mMinimumFlingVelocity;
        private int mMaximumFlingVelocity;

        private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
        private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();

        // constants for Message.what used by GestureHandler below
        private static final int SHOW_PRESS = 1;
        private static final int LONG_PRESS = 2;
        private static final int TAP = 3;

        private final Handler mHandler;
        final OnGestureListener mListener;
        OnDoubleTapListener mDoubleTapListener;

        boolean mStillDown;
        boolean mDeferConfirmSingleTap;
        private boolean mInLongPress;
        private boolean mAlwaysInTapRegion;
        private boolean mAlwaysInBiggerTapRegion;

        MotionEvent mCurrentDownEvent;
        private MotionEvent mPreviousUpEvent;

        /**
         * True when the user is still touching for the second tap (down, move, and
         * up events). Can only be true if there is a double tap listener attached.
         */
        private boolean mIsDoubleTapping;

        private float mLastFocusX;
        private float mLastFocusY;
        private float mDownFocusX;
        private float mDownFocusY;

        private boolean mIsLongpressEnabled;

        /**
         * Determines speed during touch scrolling
         */
        private VelocityTracker mVelocityTracker;

        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;

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

        /**
         * Creates a GestureDetector with the supplied listener.
         * You may only use this constructor from a UI thread (this is the usual situation).
         * @see android.os.Handler#Handler()
         *
         * @param context the application's context
         * @param listener the listener invoked for all the callbacks, this must
         * not be null.
         * @param handler the handler to use
         *
         * @throws NullPointerException if {@code listener} is null.
         */
        GestureDetectorCompatImplBase(Context context, OnGestureListener listener,
                Handler handler) {
            if (handler != null) {
                mHandler = new GestureHandler(handler);
            } else {
                mHandler = new GestureHandler();
            }
            mListener = listener;
            if (listener instanceof OnDoubleTapListener) {
                setOnDoubleTapListener((OnDoubleTapListener) listener);
            }
            init(context);
        }

        private void init(Context context) {
            if (context == null) {
                throw new IllegalArgumentException("Context must not be null");
            }
            if (mListener == null) {
                throw new IllegalArgumentException("OnGestureListener must not be null");
            }
            mIsLongpressEnabled = true;

            final ViewConfiguration configuration = ViewConfiguration.get(context);
            final int touchSlop = configuration.getScaledTouchSlop();
            final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();

            mTouchSlopSquare = touchSlop * touchSlop;
            mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
        }

        /**
         * Sets the listener which will be called for double-tap and related
         * gestures.
         *
         * @param onDoubleTapListener the listener invoked for all the callbacks, or
         *        null to stop listening for double-tap gestures.
         */
        @Override
        public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
            mDoubleTapListener = onDoubleTapListener;
        }

        /**
         * Set whether longpress is enabled, if this is enabled when a user
         * presses and holds down you get a longpress event and nothing further.
         * If it's disabled the user can press and hold down and then later
         * moved their finger and you will get scroll events. By default
         * longpress is enabled.
         *
         * @param isLongpressEnabled whether longpress should be enabled.
         */
        @Override
        public void setIsLongpressEnabled(boolean isLongpressEnabled) {
            mIsLongpressEnabled = isLongpressEnabled;
        }

        /**
         * @return true if longpress is enabled, else false.
         */
        @Override
        public boolean isLongpressEnabled() {
            return mIsLongpressEnabled;
        }

        /**
         * 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.
         */
        @SuppressWarnings("ConstantConditions")
        @Override
        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;

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

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

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

                    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) {
                        mHandler.removeMessages(LONG_PRESS);
                        mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
                                + TAP_TIMEOUT + ViewConfiguration.getLongPressTimeout());
                    }
                    mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
                            mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
                    handled |= mListener.onDown(ev);
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (mInLongPress) {
                        break;
                    }
                    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);
                        if (distance > mTouchSlopSquare) {
                            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 > mTouchSlopSquare) {
                            mAlwaysInBiggerTapRegion = false;
                        }
                    } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                        mLastFocusX = focusX;
                        mLastFocusY = focusY;
                    }
                    break;

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

                case MotionEvent.ACTION_CANCEL:
                    cancel();
                    break;
            }

            return handled;
        }

        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;
            if (mInLongPress) {
                mInLongPress = false;
            }
        }

        private void cancelTaps() {
            mHandler.removeMessages(SHOW_PRESS);
            mHandler.removeMessages(LONG_PRESS);
            mHandler.removeMessages(TAP);
            mIsDoubleTapping = false;
            mAlwaysInTapRegion = false;
            mAlwaysInBiggerTapRegion = false;
            mDeferConfirmSingleTap = false;
            if (mInLongPress) {
                mInLongPress = false;
            }
        }

        private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
                MotionEvent secondDown) {
            if (!mAlwaysInBiggerTapRegion) {
                return false;
            }

            if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
                return false;
            }

            int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
            int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
            return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
        }

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

    static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl {
        private final GestureDetector mDetector;

        GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener,
                Handler handler) {
            mDetector = new GestureDetector(context, listener, handler);
        }

        @Override
        public boolean isLongpressEnabled() {
            return mDetector.isLongpressEnabled();
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            return mDetector.onTouchEvent(ev);
        }

        @Override
        public void setIsLongpressEnabled(boolean enabled) {
            mDetector.setIsLongpressEnabled(enabled);
        }

        @Override
        public void setOnDoubleTapListener(OnDoubleTapListener listener) {
            mDetector.setOnDoubleTapListener(listener);
        }
    }

    private final GestureDetectorCompatImpl mImpl;

    /**
     * Creates a GestureDetectorCompat with the supplied listener.
     * As usual, you may only use this constructor from a UI thread.
     * @see android.os.Handler#Handler()
     *
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     */
    public GestureDetectorCompat(@NonNull Context context, @NonNull OnGestureListener listener) {
        this(context, listener, null);
    }

    /**
     * Creates a GestureDetectorCompat with the supplied listener.
     * As usual, you may only use this constructor from a UI thread.
     * @see android.os.Handler#Handler()
     *
     * @param context the application's context
     * @param listener the listener invoked for all the callbacks, this must
     * not be null.
     * @param handler the handler that will be used for posting deferred messages
     */
    public GestureDetectorCompat(@NonNull Context context, @NonNull OnGestureListener listener,
            @Nullable Handler handler) {
        if (Build.VERSION.SDK_INT > 17) {
            mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler);
        } else {
            mImpl = new GestureDetectorCompatImplBase(context, listener, handler);
        }
    }

    /**
     * @return true if longpress is enabled, else false.
     */
    public boolean isLongpressEnabled() {
        return mImpl.isLongpressEnabled();
    }

    /**
     * Analyzes the given motion event and if applicable triggers the
     * appropriate callbacks on the {@link OnGestureListener} supplied.
     *
     * @param event The current motion event.
     * @return true if the {@link OnGestureListener} consumed the event,
     *              else false.
     */
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        return mImpl.onTouchEvent(event);
    }

    /**
     * Set whether longpress is enabled, if this is enabled when a user
     * presses and holds down you get a longpress event and nothing further.
     * If it's disabled the user can press and hold down and then later
     * moved their finger and you will get scroll events. By default
     * longpress is enabled.
     *
     * @param enabled whether longpress should be enabled.
     */
    @SuppressLint("KotlinPropertyAccess")
    public void setIsLongpressEnabled(boolean enabled) {
        mImpl.setIsLongpressEnabled(enabled);
    }

    /**
     * Sets the listener which will be called for double-tap and related
     * gestures.
     *
     * @param listener the listener invoked for all the callbacks, or
     *        null to stop listening for double-tap gestures.
     */
    public void setOnDoubleTapListener(@Nullable OnDoubleTapListener listener) {
        mImpl.setOnDoubleTapListener(listener);
    }
}