| /* |
| * Copyright (C) 2008 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 android.view; |
| |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS; |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP; |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS; |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL; |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP; |
| import static android.util.StatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION; |
| |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.Context; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.os.SystemClock; |
| import android.util.StatsLog; |
| |
| /** |
| * 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). |
| * |
| * To use this class: |
| * <ul> |
| * <li>Create an instance of the {@code GestureDetector} 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. |
| * <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)} |
| * you must call {@link #onGenericMotionEvent(MotionEvent)} |
| * in {@link View#onGenericMotionEvent(MotionEvent)}. |
| * </ul> |
| */ |
| public class GestureDetector { |
| /** |
| * The listener that is used to notify when gestures occur. |
| * If you want to listen for all the different gestures then implement |
| * this interface. If you only want to listen for a subset it might |
| * be easier to extend {@link SimpleOnGestureListener}. |
| */ |
| public interface OnGestureListener { |
| |
| /** |
| * Notified when a tap occurs with the down {@link MotionEvent} |
| * that triggered it. This will be triggered immediately for |
| * every down event. All other events should be preceded by this. |
| * |
| * @param e The down motion event. |
| */ |
| boolean onDown(MotionEvent e); |
| |
| /** |
| * The user has performed a down {@link MotionEvent} and not performed |
| * a move or up yet. This event is commonly used to provide visual |
| * feedback to the user to let them know that their action has been |
| * recognized i.e. highlight an element. |
| * |
| * @param e The down motion event |
| */ |
| void onShowPress(MotionEvent e); |
| |
| /** |
| * Notified when a tap occurs with the up {@link MotionEvent} |
| * that triggered it. |
| * |
| * @param e The up motion event that completed the first tap |
| * @return true if the event is consumed, else false |
| */ |
| boolean onSingleTapUp(MotionEvent e); |
| |
| /** |
| * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the |
| * current move {@link MotionEvent}. The distance in x and y is also supplied for |
| * convenience. |
| * |
| * @param e1 The first down motion event that started the scrolling. |
| * @param e2 The move motion event that triggered the current onScroll. |
| * @param distanceX The distance along the X axis that has been scrolled since the last |
| * call to onScroll. This is NOT the distance between {@code e1} |
| * and {@code e2}. |
| * @param distanceY The distance along the Y axis that has been scrolled since the last |
| * call to onScroll. This is NOT the distance between {@code e1} |
| * and {@code e2}. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); |
| |
| /** |
| * Notified when a long press occurs with the initial on down {@link MotionEvent} |
| * that trigged it. |
| * |
| * @param e The initial on down motion event that started the longpress. |
| */ |
| void onLongPress(MotionEvent e); |
| |
| /** |
| * Notified of a fling event when it occurs with the initial on down {@link MotionEvent} |
| * and the matching up {@link MotionEvent}. The calculated velocity is supplied along |
| * the x and y axis in pixels per second. |
| * |
| * @param e1 The first down motion event that started the fling. |
| * @param e2 The move motion event that triggered the current onFling. |
| * @param velocityX The velocity of this fling measured in pixels per second |
| * along the x axis. |
| * @param velocityY The velocity of this fling measured in pixels per second |
| * along the y axis. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); |
| } |
| |
| /** |
| * The listener that is used to notify when a double-tap or a confirmed |
| * single-tap occur. |
| */ |
| public interface OnDoubleTapListener { |
| /** |
| * Notified when a single-tap occurs. |
| * <p> |
| * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this |
| * will only be called after the detector is confident that the user's |
| * first tap is not followed by a second tap leading to a double-tap |
| * gesture. |
| * |
| * @param e The down motion event of the single-tap. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onSingleTapConfirmed(MotionEvent e); |
| |
| /** |
| * Notified when a double-tap occurs. |
| * |
| * @param e The down motion event of the first tap of the double-tap. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onDoubleTap(MotionEvent e); |
| |
| /** |
| * Notified when an event within a double-tap gesture occurs, including |
| * the down, move, and up events. |
| * |
| * @param e The motion event that occurred during the double-tap gesture. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onDoubleTapEvent(MotionEvent e); |
| } |
| |
| /** |
| * The listener that is used to notify when a context click occurs. When listening for a |
| * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in |
| * {@link View#onGenericMotionEvent(MotionEvent)}. |
| */ |
| public interface OnContextClickListener { |
| /** |
| * Notified when a context click occurs. |
| * |
| * @param e The motion event that occurred during the context click. |
| * @return true if the event is consumed, else false |
| */ |
| boolean onContextClick(MotionEvent e); |
| } |
| |
| /** |
| * A convenience class to extend when you only want to listen for a subset |
| * of all the gestures. This implements all methods in the |
| * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener} |
| * but does nothing and return {@code false} for all applicable methods. |
| */ |
| public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener, |
| OnContextClickListener { |
| |
| public boolean onSingleTapUp(MotionEvent e) { |
| return false; |
| } |
| |
| public void onLongPress(MotionEvent e) { |
| } |
| |
| public boolean onScroll(MotionEvent e1, MotionEvent e2, |
| float distanceX, float distanceY) { |
| return false; |
| } |
| |
| public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, |
| float velocityY) { |
| return false; |
| } |
| |
| public void onShowPress(MotionEvent e) { |
| } |
| |
| public boolean onDown(MotionEvent e) { |
| return false; |
| } |
| |
| public boolean onDoubleTap(MotionEvent e) { |
| return false; |
| } |
| |
| public boolean onDoubleTapEvent(MotionEvent e) { |
| return false; |
| } |
| |
| public boolean onSingleTapConfirmed(MotionEvent e) { |
| return false; |
| } |
| |
| public boolean onContextClick(MotionEvent e) { |
| return false; |
| } |
| } |
| |
| @UnsupportedAppUsage |
| private int mTouchSlopSquare; |
| private int mDoubleTapTouchSlopSquare; |
| private int mDoubleTapSlopSquare; |
| @UnsupportedAppUsage |
| private int mMinimumFlingVelocity; |
| private int mMaximumFlingVelocity; |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) |
| private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); |
| private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); |
| private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); |
| private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime(); |
| |
| // 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; |
| @UnsupportedAppUsage |
| private final OnGestureListener mListener; |
| private OnDoubleTapListener mDoubleTapListener; |
| private OnContextClickListener mContextClickListener; |
| |
| private boolean mStillDown; |
| private boolean mDeferConfirmSingleTap; |
| private boolean mInLongPress; |
| private boolean mInContextClick; |
| @UnsupportedAppUsage |
| private boolean mAlwaysInTapRegion; |
| private boolean mAlwaysInBiggerTapRegion; |
| private boolean mIgnoreNextUpEvent; |
| // Whether a classification has been recorded by statsd for the current event stream. Reset on |
| // ACTION_DOWN. |
| private boolean mHasRecordedClassification; |
| |
| private MotionEvent mCurrentDownEvent; |
| private MotionEvent mCurrentMotionEvent; |
| 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; |
| |
| /** |
| * Consistency verifier for debugging purposes. |
| */ |
| private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = |
| InputEventConsistencyVerifier.isInstrumentationEnabled() ? |
| new InputEventConsistencyVerifier(this, 0) : null; |
| |
| 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: |
| 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 |
| } |
| } |
| } |
| |
| /** |
| * Creates a GestureDetector with the supplied listener. |
| * This variant of the constructor should be used from a non-UI thread |
| * (as it allows specifying the Handler). |
| * |
| * @param listener the listener invoked for all the callbacks, this must |
| * not be null. |
| * @param handler the handler to use |
| * |
| * @throws NullPointerException if either {@code listener} or |
| * {@code handler} is null. |
| * |
| * @deprecated Use {@link #GestureDetector(android.content.Context, |
| * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead. |
| */ |
| @Deprecated |
| public GestureDetector(OnGestureListener listener, Handler handler) { |
| this(null, listener, handler); |
| } |
| |
| /** |
| * 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 listener the listener invoked for all the callbacks, this must |
| * not be null. |
| * |
| * @throws NullPointerException if {@code listener} is null. |
| * |
| * @deprecated Use {@link #GestureDetector(android.content.Context, |
| * android.view.GestureDetector.OnGestureListener)} instead. |
| */ |
| @Deprecated |
| public GestureDetector(OnGestureListener listener) { |
| this(null, listener, null); |
| } |
| |
| /** |
| * Creates a GestureDetector with the supplied listener. |
| * You may only use this constructor from a {@link android.os.Looper} 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. |
| * |
| * @throws NullPointerException if {@code listener} is null. |
| */ |
| public GestureDetector(Context context, OnGestureListener listener) { |
| this(context, listener, null); |
| } |
| |
| /** |
| * Creates a GestureDetector with the supplied listener that runs deferred events on the |
| * thread associated with the supplied {@link android.os.Handler}. |
| * @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 for running deferred listener events. |
| * |
| * @throws NullPointerException if {@code listener} is null. |
| */ |
| public GestureDetector(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); |
| } |
| if (listener instanceof OnContextClickListener) { |
| setContextClickListener((OnContextClickListener) listener); |
| } |
| init(context); |
| } |
| |
| /** |
| * Creates a GestureDetector with the supplied listener that runs deferred events on the |
| * thread associated with the supplied {@link android.os.Handler}. |
| * @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 for running deferred listener events. |
| * @param unused currently not used. |
| * |
| * @throws NullPointerException if {@code listener} is null. |
| */ |
| public GestureDetector(Context context, OnGestureListener listener, Handler handler, |
| boolean unused) { |
| this(context, listener, handler); |
| } |
| |
| private void init(Context context) { |
| if (mListener == null) { |
| throw new NullPointerException("OnGestureListener must not be null"); |
| } |
| mIsLongpressEnabled = true; |
| |
| // Fallback to support pre-donuts releases |
| int touchSlop, doubleTapSlop, doubleTapTouchSlop; |
| if (context == null) { |
| //noinspection deprecation |
| touchSlop = ViewConfiguration.getTouchSlop(); |
| doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this |
| doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); |
| //noinspection deprecation |
| mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); |
| mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); |
| } else { |
| final ViewConfiguration configuration = ViewConfiguration.get(context); |
| touchSlop = configuration.getScaledTouchSlop(); |
| doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop(); |
| doubleTapSlop = configuration.getScaledDoubleTapSlop(); |
| mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); |
| mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); |
| } |
| mTouchSlopSquare = touchSlop * touchSlop; |
| mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop; |
| 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. |
| */ |
| public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { |
| mDoubleTapListener = onDoubleTapListener; |
| } |
| |
| /** |
| * Sets the listener which will be called for context clicks. |
| * |
| * @param onContextClickListener the listener invoked for all the callbacks, or null to stop |
| * listening for context clicks. |
| */ |
| public void setContextClickListener(OnContextClickListener onContextClickListener) { |
| mContextClickListener = onContextClickListener; |
| } |
| |
| /** |
| * 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. |
| */ |
| public void setIsLongpressEnabled(boolean isLongpressEnabled) { |
| mIsLongpressEnabled = isLongpressEnabled; |
| } |
| |
| /** |
| * @return true if longpress is enabled, else false. |
| */ |
| 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. |
| */ |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mInputEventConsistencyVerifier != null) { |
| mInputEventConsistencyVerifier.onTouchEvent(ev, 0); |
| } |
| |
| final int action = ev.getAction(); |
| |
| if (mCurrentMotionEvent != null) { |
| mCurrentMotionEvent.recycle(); |
| } |
| mCurrentMotionEvent = MotionEvent.obtain(ev); |
| |
| 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: |
| 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; |
| recordGestureClassification( |
| TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP); |
| // 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; |
| |
| 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); |
| handled |= mListener.onDown(ev); |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| if (mInLongPress || mInContextClick) { |
| break; |
| } |
| |
| final int motionClassification = ev.getClassification(); |
| final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS); |
| |
| final float scrollX = mLastFocusX - focusX; |
| final float scrollY = mLastFocusY - focusY; |
| if (mIsDoubleTapping) { |
| // Give the move events of the double-tap |
| recordGestureClassification( |
| TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__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; |
| |
| final boolean ambiguousGesture = |
| motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE; |
| final boolean shouldInhibitDefaultAction = |
| hasPendingLongPress && ambiguousGesture; |
| if (shouldInhibitDefaultAction) { |
| // Inhibit default long press |
| final float multiplier = ViewConfiguration.getAmbiguousGestureMultiplier(); |
| if (distance > slopSquare) { |
| // 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. |
| 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)); |
| } |
| // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll |
| // until the gesture is resolved. |
| // However, for safety, simply increase the touch slop in case the |
| // classification is erroneous. Since the value is squared, multiply twice. |
| slopSquare *= multiplier * multiplier; |
| } |
| |
| if (distance > slopSquare) { |
| recordGestureClassification( |
| TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL); |
| handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); |
| mLastFocusX = focusX; |
| mLastFocusY = focusY; |
| mAlwaysInTapRegion = false; |
| mHandler.removeMessages(TAP); |
| mHandler.removeMessages(SHOW_PRESS); |
| mHandler.removeMessages(LONG_PRESS); |
| } |
| int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare; |
| if (distance > doubleTapSlopSquare) { |
| mAlwaysInBiggerTapRegion = false; |
| } |
| } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { |
| recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL); |
| handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); |
| mLastFocusX = focusX; |
| mLastFocusY = focusY; |
| } |
| 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 */)); |
| } |
| break; |
| |
| case MotionEvent.ACTION_UP: |
| mStillDown = false; |
| MotionEvent currentUpEvent = MotionEvent.obtain(ev); |
| if (mIsDoubleTapping) { |
| // Finally, give the up event of the double-tap |
| recordGestureClassification( |
| TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP); |
| handled |= mDoubleTapListener.onDoubleTapEvent(ev); |
| } else if (mInLongPress) { |
| mHandler.removeMessages(TAP); |
| mInLongPress = false; |
| } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) { |
| recordGestureClassification( |
| TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP); |
| 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); |
| } |
| } |
| 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; |
| mIgnoreNextUpEvent = false; |
| mHandler.removeMessages(SHOW_PRESS); |
| mHandler.removeMessages(LONG_PRESS); |
| break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| cancel(); |
| break; |
| } |
| |
| if (!handled && mInputEventConsistencyVerifier != null) { |
| mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0); |
| } |
| return handled; |
| } |
| |
| /** |
| * Analyzes the given generic 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 onGenericMotionEvent(MotionEvent ev) { |
| if (mInputEventConsistencyVerifier != null) { |
| mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0); |
| } |
| |
| final int actionButton = ev.getActionButton(); |
| switch (ev.getActionMasked()) { |
| case MotionEvent.ACTION_BUTTON_PRESS: |
| if (mContextClickListener != null && !mInContextClick && !mInLongPress |
| && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY |
| || actionButton == MotionEvent.BUTTON_SECONDARY)) { |
| if (mContextClickListener.onContextClick(ev)) { |
| mInContextClick = true; |
| mHandler.removeMessages(LONG_PRESS); |
| mHandler.removeMessages(TAP); |
| return true; |
| } |
| } |
| break; |
| |
| case MotionEvent.ACTION_BUTTON_RELEASE: |
| if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY |
| || actionButton == MotionEvent.BUTTON_SECONDARY)) { |
| mInContextClick = false; |
| mIgnoreNextUpEvent = true; |
| } |
| break; |
| } |
| return 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; |
| } |
| |
| 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 boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, |
| MotionEvent secondDown) { |
| if (!mAlwaysInBiggerTapRegion) { |
| return false; |
| } |
| |
| final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime(); |
| if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) { |
| return false; |
| } |
| |
| int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); |
| int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); |
| final boolean isGeneratedGesture = |
| (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0; |
| int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare; |
| return (deltaX * deltaX + deltaY * deltaY < slopSquare); |
| } |
| |
| private void dispatchLongPress() { |
| mHandler.removeMessages(TAP); |
| mDeferConfirmSingleTap = false; |
| mInLongPress = true; |
| mListener.onLongPress(mCurrentDownEvent); |
| } |
| |
| private void recordGestureClassification(int classification) { |
| if (mHasRecordedClassification |
| || classification |
| == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) { |
| // Only record the first classification for an event stream. |
| return; |
| } |
| if (mCurrentDownEvent == null || mCurrentMotionEvent == null) { |
| // If the complete event stream wasn't seen, don't record anything. |
| mHasRecordedClassification = true; |
| return; |
| } |
| StatsLog.write( |
| StatsLog.TOUCH_GESTURE_CLASSIFIED, |
| getClass().getName(), |
| classification, |
| (int) (SystemClock.uptimeMillis() - mCurrentMotionEvent.getDownTime()), |
| (float) Math.hypot(mCurrentMotionEvent.getRawX() - mCurrentDownEvent.getRawX(), |
| mCurrentMotionEvent.getRawY() - mCurrentDownEvent.getRawY())); |
| mHasRecordedClassification = true; |
| } |
| } |