• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
20 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP;
21 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
22 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL;
23 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
24 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.annotation.UiContext;
29 import android.app.Activity;
30 import android.compat.annotation.UnsupportedAppUsage;
31 import android.content.Context;
32 import android.os.Build;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.os.Message;
37 import android.os.StrictMode;
38 import android.os.SystemClock;
39 
40 import com.android.internal.util.FrameworkStatsLog;
41 
42 /**
43  * Detects various gestures and events using the supplied {@link MotionEvent}s.
44  * The {@link OnGestureListener} callback will notify users when a particular
45  * motion event has occurred. This class should only be used with {@link MotionEvent}s
46  * reported via touch (don't use for trackball events).
47  *
48  * To use this class:
49  * <ul>
50  *  <li>Create an instance of the {@code GestureDetector} for your {@link View}
51  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
52  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
53  *          will be executed when the events occur.
54  *  <li>If listening for {@link OnContextClickListener#onContextClick(MotionEvent)}
55  *          you must call {@link #onGenericMotionEvent(MotionEvent)}
56  *          in {@link View#onGenericMotionEvent(MotionEvent)}.
57  * </ul>
58  */
59 public class GestureDetector {
60     /**
61      * The listener that is used to notify when gestures occur.
62      * If you want to listen for all the different gestures then implement
63      * this interface. If you only want to listen for a subset it might
64      * be easier to extend {@link SimpleOnGestureListener}.
65      */
66     public interface OnGestureListener {
67 
68         /**
69          * Notified when a tap occurs with the down {@link MotionEvent}
70          * that triggered it. This will be triggered immediately for
71          * every down event. All other events should be preceded by this.
72          *
73          * @param e The down motion event.
74          */
onDown(@onNull MotionEvent e)75         boolean onDown(@NonNull MotionEvent e);
76 
77         /**
78          * The user has performed a down {@link MotionEvent} and not performed
79          * a move or up yet. This event is commonly used to provide visual
80          * feedback to the user to let them know that their action has been
81          * recognized i.e. highlight an element.
82          *
83          * @param e The down motion event
84          */
onShowPress(@onNull MotionEvent e)85         void onShowPress(@NonNull MotionEvent e);
86 
87         /**
88          * Notified when a tap occurs with the up {@link MotionEvent}
89          * that triggered it.
90          *
91          * @param e The up motion event that completed the first tap
92          * @return true if the event is consumed, else false
93          */
onSingleTapUp(@onNull MotionEvent e)94         boolean onSingleTapUp(@NonNull MotionEvent e);
95 
96         /**
97          * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
98          * current move {@link MotionEvent}. The distance in x and y is also supplied for
99          * convenience.
100          *
101          * @param e1 The first down motion event that started the scrolling. A {@code null} event
102          *           indicates an incomplete event stream or error state.
103          * @param e2 The move motion event that triggered the current onScroll.
104          * @param distanceX The distance along the X axis that has been scrolled since the last
105          *              call to onScroll. This is NOT the distance between {@code e1}
106          *              and {@code e2}.
107          * @param distanceY The distance along the Y axis that has been scrolled since the last
108          *              call to onScroll. This is NOT the distance between {@code e1}
109          *              and {@code e2}.
110          * @return true if the event is consumed, else false
111          */
onScroll(@ullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY)112         boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX,
113                 float distanceY);
114 
115         /**
116          * Notified when a long press occurs with the initial on down {@link MotionEvent}
117          * that trigged it.
118          *
119          * @param e The initial on down motion event that started the longpress.
120          */
onLongPress(@onNull MotionEvent e)121         void onLongPress(@NonNull MotionEvent e);
122 
123         /**
124          * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
125          * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
126          * the x and y axis in pixels per second.
127          *
128          * @param e1 The first down motion event that started the fling. A {@code null} event
129          *           indicates an incomplete event stream or error state.
130          * @param e2 The move motion event that triggered the current onFling.
131          * @param velocityX The velocity of this fling measured in pixels per second
132          *              along the x axis.
133          * @param velocityY The velocity of this fling measured in pixels per second
134          *              along the y axis.
135          * @return true if the event is consumed, else false
136          */
onFling(@ullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY)137         boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
138                 float velocityY);
139     }
140 
141     /**
142      * The listener that is used to notify when a double-tap or a confirmed
143      * single-tap occur.
144      */
145     public interface OnDoubleTapListener {
146         /**
147          * Notified when a single-tap occurs.
148          * <p>
149          * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
150          * will only be called after the detector is confident that the user's
151          * first tap is not followed by a second tap leading to a double-tap
152          * gesture.
153          *
154          * @param e The down motion event of the single-tap.
155          * @return true if the event is consumed, else false
156          */
onSingleTapConfirmed(@onNull MotionEvent e)157         boolean onSingleTapConfirmed(@NonNull MotionEvent e);
158 
159         /**
160          * Notified when a double-tap occurs. Triggered on the down event of second tap.
161          *
162          * @param e The down motion event of the first tap of the double-tap.
163          * @return true if the event is consumed, else false
164          */
onDoubleTap(@onNull MotionEvent e)165         boolean onDoubleTap(@NonNull MotionEvent e);
166 
167         /**
168          * Notified when an event within a double-tap gesture occurs, including
169          * the down, move, and up events.
170          *
171          * @param e The motion event that occurred during the double-tap gesture.
172          * @return true if the event is consumed, else false
173          */
onDoubleTapEvent(@onNull MotionEvent e)174         boolean onDoubleTapEvent(@NonNull MotionEvent e);
175     }
176 
177     /**
178      * The listener that is used to notify when a context click occurs. When listening for a
179      * context click ensure that you call {@link #onGenericMotionEvent(MotionEvent)} in
180      * {@link View#onGenericMotionEvent(MotionEvent)}.
181      */
182     public interface OnContextClickListener {
183         /**
184          * Notified when a context click occurs.
185          *
186          * @param e The motion event that occurred during the context click.
187          * @return true if the event is consumed, else false
188          */
onContextClick(@onNull MotionEvent e)189         boolean onContextClick(@NonNull MotionEvent e);
190     }
191 
192     /**
193      * A convenience class to extend when you only want to listen for a subset
194      * of all the gestures. This implements all methods in the
195      * {@link OnGestureListener}, {@link OnDoubleTapListener}, and {@link OnContextClickListener}
196      * but does nothing and return {@code false} for all applicable methods.
197      */
198     public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
199             OnContextClickListener {
200 
onSingleTapUp(@onNull MotionEvent e)201         public boolean onSingleTapUp(@NonNull MotionEvent e) {
202             return false;
203         }
204 
onLongPress(@onNull MotionEvent e)205         public void onLongPress(@NonNull MotionEvent e) {
206         }
207 
onScroll(@ullable MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY)208         public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2,
209                 float distanceX, float distanceY) {
210             return false;
211         }
212 
onFling(@ullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY)213         public boolean onFling(@Nullable MotionEvent e1, @NonNull MotionEvent e2, float velocityX,
214                 float velocityY) {
215             return false;
216         }
217 
onShowPress(@onNull MotionEvent e)218         public void onShowPress(@NonNull MotionEvent e) {
219         }
220 
onDown(@onNull MotionEvent e)221         public boolean onDown(@NonNull MotionEvent e) {
222             return false;
223         }
224 
onDoubleTap(@onNull MotionEvent e)225         public boolean onDoubleTap(@NonNull MotionEvent e) {
226             return false;
227         }
228 
onDoubleTapEvent(@onNull MotionEvent e)229         public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
230             return false;
231         }
232 
onSingleTapConfirmed(@onNull MotionEvent e)233         public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
234             return false;
235         }
236 
onContextClick(@onNull MotionEvent e)237         public boolean onContextClick(@NonNull MotionEvent e) {
238             return false;
239         }
240     }
241 
242     private static final String TAG = GestureDetector.class.getSimpleName();
243     @UnsupportedAppUsage
244     private int mTouchSlopSquare;
245     private int mDoubleTapTouchSlopSquare;
246     private int mDoubleTapSlopSquare;
247     private float mAmbiguousGestureMultiplier;
248     @UnsupportedAppUsage
249     private int mMinimumFlingVelocity;
250     private int mMaximumFlingVelocity;
251 
252     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
253     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
254     private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
255     private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
256     private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();
257 
258     // constants for Message.what used by GestureHandler below
259     private static final int SHOW_PRESS = 1;
260     private static final int LONG_PRESS = 2;
261     private static final int TAP = 3;
262 
263     private final Handler mHandler;
264     @UnsupportedAppUsage
265     private final OnGestureListener mListener;
266     private OnDoubleTapListener mDoubleTapListener;
267     private OnContextClickListener mContextClickListener;
268 
269     private boolean mStillDown;
270     private boolean mDeferConfirmSingleTap;
271     private boolean mInLongPress;
272     private boolean mInContextClick;
273     @UnsupportedAppUsage
274     private boolean mAlwaysInTapRegion;
275     private boolean mAlwaysInBiggerTapRegion;
276     private boolean mIgnoreNextUpEvent;
277     // Whether a classification has been recorded by statsd for the current event stream. Reset on
278     // ACTION_DOWN.
279     private boolean mHasRecordedClassification;
280 
281     private MotionEvent mCurrentDownEvent;
282     private MotionEvent mCurrentMotionEvent;
283     private MotionEvent mPreviousUpEvent;
284 
285     /**
286      * True when the user is still touching for the second tap (down, move, and
287      * up events). Can only be true if there is a double tap listener attached.
288      */
289     private boolean mIsDoubleTapping;
290 
291     private float mLastFocusX;
292     private float mLastFocusY;
293     private float mDownFocusX;
294     private float mDownFocusY;
295 
296     private boolean mIsLongpressEnabled;
297 
298     /**
299      * Determines speed during touch scrolling
300      */
301     private VelocityTracker mVelocityTracker;
302 
303     /**
304      * Determines strategy for velocity calculation
305      */
306     private @VelocityTracker.VelocityTrackerStrategy int mVelocityTrackerStrategy;
307 
308     /**
309      * Consistency verifier for debugging purposes.
310      */
311     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
312             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
313                     new InputEventConsistencyVerifier(this, 0) : null;
314 
315     private class GestureHandler extends Handler {
GestureHandler()316         GestureHandler() {
317             super();
318         }
319 
GestureHandler(Handler handler)320         GestureHandler(Handler handler) {
321             super(handler.getLooper());
322         }
323 
324         @Override
handleMessage(Message msg)325         public void handleMessage(Message msg) {
326             switch (msg.what) {
327                 case SHOW_PRESS:
328                     mListener.onShowPress(mCurrentDownEvent);
329                     break;
330 
331                 case LONG_PRESS:
332                     recordGestureClassification(msg.arg1);
333                     dispatchLongPress();
334                     break;
335 
336                 case TAP:
337                     // If the user's finger is still down, do not count it as a tap
338                     if (mDoubleTapListener != null) {
339                         if (!mStillDown) {
340                             recordGestureClassification(
341                                     TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
342                             mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
343                         } else {
344                             mDeferConfirmSingleTap = true;
345                         }
346                     }
347                     break;
348 
349                 default:
350                     throw new RuntimeException("Unknown message " + msg); //never
351             }
352         }
353     }
354 
355     /**
356      * Creates a GestureDetector with the supplied listener.
357      * This variant of the constructor should be used from a non-UI thread
358      * (as it allows specifying the Handler).
359      *
360      * @param listener the listener invoked for all the callbacks, this must
361      * not be null.
362      * @param handler the handler to use
363      *
364      * @throws NullPointerException if {@code listener} is null.
365      *
366      * @deprecated Use {@link #GestureDetector(Context, GestureDetector.OnGestureListener, Handler)}
367      * instead.
368      */
369     @Deprecated
GestureDetector(@onNull OnGestureListener listener, @Nullable Handler handler)370     public GestureDetector(@NonNull OnGestureListener listener, @Nullable Handler handler) {
371         this(null, listener, handler);
372     }
373 
374     /**
375      * Creates a GestureDetector with the supplied listener.
376      * You may only use this constructor from a UI thread (this is the usual situation).
377      * @see Handler#Handler()
378      *
379      * @param listener the listener invoked for all the callbacks, this must
380      * not be null.
381      *
382      * @throws NullPointerException if {@code listener} is null.
383      *
384      * @deprecated Use {@link #GestureDetector(Context, GestureDetector.OnGestureListener)} instead.
385      */
386     @Deprecated
GestureDetector(@onNull OnGestureListener listener)387     public GestureDetector(@NonNull OnGestureListener listener) {
388         this(null, listener, null);
389     }
390 
391     /**
392      * Creates a GestureDetector with the supplied listener.
393      * You may only use this constructor from a {@link Looper} thread.
394      * @see Handler#Handler()
395      *
396      * @param context An {@link Activity} or a {@link Context} created from
397      * {@link Context#createWindowContext(int, Bundle)}
398      * @param listener the listener invoked for all the callbacks, this must
399      * not be null. If the listener implements the {@link OnDoubleTapListener} or
400      * {@link OnContextClickListener} then it will also be set as the listener for
401      * these callbacks (for example when using the {@link SimpleOnGestureListener}).
402      *
403      * @throws NullPointerException if {@code listener} is null.
404      */
405     // TODO(b/182007470): Use @ConfigurationContext instead
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener)406     public GestureDetector(@Nullable @UiContext Context context,
407             @NonNull OnGestureListener listener) {
408         this(context, listener, null);
409     }
410 
411     /**
412      * Creates a GestureDetector with the supplied listener that runs deferred events on the
413      * thread associated with the supplied {@link Handler}.
414      * @see Handler#Handler()
415      *
416      * @param context An {@link Activity} or a {@link Context} created from
417      * {@link Context#createWindowContext(int, Bundle)}
418      * @param listener the listener invoked for all the callbacks, this must
419      * not be null. If the listener implements the {@link OnDoubleTapListener} or
420      * {@link OnContextClickListener} then it will also be set as the listener for
421      * these callbacks (for example when using the {@link SimpleOnGestureListener}).
422      * @param handler the handler to use for running deferred listener events.
423      *
424      * @throws NullPointerException if {@code listener} is null.
425      */
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener, @Nullable Handler handler)426     public GestureDetector(@Nullable @UiContext Context context,
427             @NonNull OnGestureListener listener, @Nullable Handler handler) {
428         this(context, listener, handler, VelocityTracker.VELOCITY_TRACKER_STRATEGY_DEFAULT);
429     }
430 
431     /**
432      * Creates a GestureDetector with the supplied listener that runs deferred events on the
433      * thread associated with the supplied {@link Handler}.
434      * @see Handler#Handler()
435      *
436      * @param context An {@link Activity} or a {@link Context} created from
437      * {@link Context#createWindowContext(int, Bundle)}
438      * @param listener the listener invoked for all the callbacks, this must
439      * not be null. If the listener implements the {@link OnDoubleTapListener} or
440      * {@link OnContextClickListener} then it will also be set as the listener for
441      * these callbacks (for example when using the {@link SimpleOnGestureListener}).
442      * @param handler the handler to use for running deferred listener events.
443      * @param velocityTrackerStrategy strategy to use for velocity calculation of scroll/fling
444      *                                events.
445      *
446      * @throws NullPointerException if {@code listener} is null.
447      *
448      * @hide
449      */
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener, @Nullable Handler handler, @VelocityTracker.VelocityTrackerStrategy int velocityTrackerStrategy)450     public GestureDetector(@Nullable @UiContext Context context,
451             @NonNull OnGestureListener listener, @Nullable Handler handler,
452             @VelocityTracker.VelocityTrackerStrategy int velocityTrackerStrategy) {
453         if (handler != null) {
454             mHandler = new GestureHandler(handler);
455         } else {
456             mHandler = new GestureHandler();
457         }
458         mListener = listener;
459         if (listener instanceof OnDoubleTapListener) {
460             setOnDoubleTapListener((OnDoubleTapListener) listener);
461         }
462         if (listener instanceof OnContextClickListener) {
463             setContextClickListener((OnContextClickListener) listener);
464         }
465         mVelocityTrackerStrategy = velocityTrackerStrategy;
466         init(context);
467     }
468 
469     /**
470      * Creates a GestureDetector with the supplied listener that runs deferred events on the
471      * thread associated with the supplied {@link Handler}.
472      * @see Handler#Handler()
473      *
474      * @param context An {@link Activity} or a {@link Context} created from
475      * {@link Context#createWindowContext(int, Bundle)}
476      * @param listener the listener invoked for all the callbacks, this must
477      * not be null.
478      * @param handler the handler to use for running deferred listener events.
479      * @param unused currently not used.
480      *
481      * @throws NullPointerException if {@code listener} is null.
482      */
GestureDetector(@ullable @iContext Context context, @NonNull OnGestureListener listener, @Nullable Handler handler, boolean unused)483     public GestureDetector(@Nullable @UiContext Context context,
484             @NonNull OnGestureListener listener, @Nullable Handler handler, boolean unused) {
485         this(context, listener, handler);
486     }
487 
init(@iContext Context context)488     private void init(@UiContext Context context) {
489         if (mListener == null) {
490             throw new NullPointerException("OnGestureListener must not be null");
491         }
492         mIsLongpressEnabled = true;
493 
494         // Fallback to support pre-donuts releases
495         int touchSlop, doubleTapSlop, doubleTapTouchSlop;
496         if (context == null) {
497             //noinspection deprecation
498             touchSlop = ViewConfiguration.getTouchSlop();
499             doubleTapTouchSlop = touchSlop; // Hack rather than adding a hidden method for this
500             doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
501             //noinspection deprecation
502             mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
503             mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
504             mAmbiguousGestureMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier();
505         } else {
506             StrictMode.assertConfigurationContext(context, "GestureDetector#init");
507             final ViewConfiguration configuration = ViewConfiguration.get(context);
508             touchSlop = configuration.getScaledTouchSlop();
509             doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
510             doubleTapSlop = configuration.getScaledDoubleTapSlop();
511             mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
512             mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
513             mAmbiguousGestureMultiplier = configuration.getScaledAmbiguousGestureMultiplier();
514         }
515         mTouchSlopSquare = touchSlop * touchSlop;
516         mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
517         mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
518     }
519 
520     /**
521      * Sets the listener which will be called for double-tap and related
522      * gestures.
523      *
524      * @param onDoubleTapListener the listener invoked for all the callbacks, or
525      *        null to stop listening for double-tap gestures.
526      */
setOnDoubleTapListener(@ullable OnDoubleTapListener onDoubleTapListener)527     public void setOnDoubleTapListener(@Nullable OnDoubleTapListener onDoubleTapListener) {
528         mDoubleTapListener = onDoubleTapListener;
529     }
530 
531     /**
532      * Sets the listener which will be called for context clicks.
533      *
534      * @param onContextClickListener the listener invoked for all the callbacks, or null to stop
535      *            listening for context clicks.
536      */
setContextClickListener(@ullable OnContextClickListener onContextClickListener)537     public void setContextClickListener(@Nullable OnContextClickListener onContextClickListener) {
538         mContextClickListener = onContextClickListener;
539     }
540 
541     /**
542      * Set whether longpress is enabled, if this is enabled when a user
543      * presses and holds down you get a longpress event and nothing further.
544      * If it's disabled the user can press and hold down and then later
545      * moved their finger and you will get scroll events. By default
546      * longpress is enabled.
547      *
548      * @param isLongpressEnabled whether longpress should be enabled.
549      */
setIsLongpressEnabled(boolean isLongpressEnabled)550     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
551         mIsLongpressEnabled = isLongpressEnabled;
552     }
553 
554     /**
555      * @return true if longpress is enabled, else false.
556      */
isLongpressEnabled()557     public boolean isLongpressEnabled() {
558         return mIsLongpressEnabled;
559     }
560 
561     /**
562      * Analyzes the given motion event and if applicable triggers the
563      * appropriate callbacks on the {@link OnGestureListener} supplied.
564      *
565      * @param ev The current motion event.
566      * @return true if the {@link OnGestureListener} consumed the event,
567      *              else false.
568      */
onTouchEvent(@onNull MotionEvent ev)569     public boolean onTouchEvent(@NonNull MotionEvent ev) {
570         if (mInputEventConsistencyVerifier != null) {
571             mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
572         }
573 
574         final int action = ev.getAction();
575 
576         if (mCurrentMotionEvent != null) {
577             mCurrentMotionEvent.recycle();
578         }
579         mCurrentMotionEvent = MotionEvent.obtain(ev);
580 
581         if (mVelocityTracker == null) {
582             mVelocityTracker = VelocityTracker.obtain(mVelocityTrackerStrategy);
583         }
584         mVelocityTracker.addMovement(ev);
585 
586         final boolean pointerUp =
587                 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
588         final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
589         final boolean isGeneratedGesture =
590                 (ev.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
591 
592         // Determine focal point
593         float sumX = 0, sumY = 0;
594         final int count = ev.getPointerCount();
595         for (int i = 0; i < count; i++) {
596             if (skipIndex == i) continue;
597             sumX += ev.getX(i);
598             sumY += ev.getY(i);
599         }
600         final int div = pointerUp ? count - 1 : count;
601         final float focusX = sumX / div;
602         final float focusY = sumY / div;
603 
604         boolean handled = false;
605 
606         switch (action & MotionEvent.ACTION_MASK) {
607             case MotionEvent.ACTION_POINTER_DOWN:
608                 mDownFocusX = mLastFocusX = focusX;
609                 mDownFocusY = mLastFocusY = focusY;
610                 // Cancel long press and taps
611                 cancelTaps();
612                 break;
613 
614             case MotionEvent.ACTION_POINTER_UP:
615                 mDownFocusX = mLastFocusX = focusX;
616                 mDownFocusY = mLastFocusY = focusY;
617 
618                 // Check the dot product of current velocities.
619                 // If the pointer that left was opposing another velocity vector, clear.
620                 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
621                 final int upIndex = ev.getActionIndex();
622                 final int id1 = ev.getPointerId(upIndex);
623                 final float x1 = mVelocityTracker.getXVelocity(id1);
624                 final float y1 = mVelocityTracker.getYVelocity(id1);
625                 for (int i = 0; i < count; i++) {
626                     if (i == upIndex) continue;
627 
628                     final int id2 = ev.getPointerId(i);
629                     final float x = x1 * mVelocityTracker.getXVelocity(id2);
630                     final float y = y1 * mVelocityTracker.getYVelocity(id2);
631 
632                     final float dot = x + y;
633                     if (dot < 0) {
634                         mVelocityTracker.clear();
635                         break;
636                     }
637                 }
638                 break;
639 
640             case MotionEvent.ACTION_DOWN:
641                 if (mDoubleTapListener != null) {
642                     boolean hadTapMessage = mHandler.hasMessages(TAP);
643                     if (hadTapMessage) mHandler.removeMessages(TAP);
644                     if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
645                             && hadTapMessage
646                             && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
647                         // This is a second tap
648                         mIsDoubleTapping = true;
649                         recordGestureClassification(
650                                 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
651                         // Give a callback with the first tap of the double-tap
652                         handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
653                         // Give a callback with down event of the double-tap
654                         handled |= mDoubleTapListener.onDoubleTapEvent(ev);
655                     } else {
656                         // This is a first tap
657                         mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
658                     }
659                 }
660 
661                 mDownFocusX = mLastFocusX = focusX;
662                 mDownFocusY = mLastFocusY = focusY;
663                 if (mCurrentDownEvent != null) {
664                     mCurrentDownEvent.recycle();
665                 }
666                 mCurrentDownEvent = MotionEvent.obtain(ev);
667                 mAlwaysInTapRegion = true;
668                 mAlwaysInBiggerTapRegion = true;
669                 mStillDown = true;
670                 mInLongPress = false;
671                 mDeferConfirmSingleTap = false;
672                 mHasRecordedClassification = false;
673 
674                 if (mIsLongpressEnabled) {
675                     mHandler.removeMessages(LONG_PRESS);
676                     mHandler.sendMessageAtTime(
677                             mHandler.obtainMessage(
678                                     LONG_PRESS,
679                                     TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
680                                     0 /* arg2 */),
681                             mCurrentDownEvent.getDownTime()
682                                     + ViewConfiguration.getLongPressTimeout());
683                 }
684                 mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
685                         mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
686                 handled |= mListener.onDown(ev);
687                 break;
688 
689             case MotionEvent.ACTION_MOVE:
690                 if (mInLongPress || mInContextClick) {
691                     break;
692                 }
693 
694                 final int motionClassification = ev.getClassification();
695                 final boolean hasPendingLongPress = mHandler.hasMessages(LONG_PRESS);
696 
697                 final float scrollX = mLastFocusX - focusX;
698                 final float scrollY = mLastFocusY - focusY;
699                 if (mIsDoubleTapping) {
700                     // Give the move events of the double-tap
701                     recordGestureClassification(
702                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
703                     handled |= mDoubleTapListener.onDoubleTapEvent(ev);
704                 } else if (mAlwaysInTapRegion) {
705                     final int deltaX = (int) (focusX - mDownFocusX);
706                     final int deltaY = (int) (focusY - mDownFocusY);
707                     int distance = (deltaX * deltaX) + (deltaY * deltaY);
708                     int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
709 
710                     final boolean ambiguousGesture =
711                             motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
712                     final boolean shouldInhibitDefaultAction =
713                             hasPendingLongPress && ambiguousGesture;
714                     if (shouldInhibitDefaultAction) {
715                         // Inhibit default long press
716                         if (distance > slopSquare) {
717                             // The default action here is to remove long press. But if the touch
718                             // slop below gets increased, and we never exceed the modified touch
719                             // slop while still receiving AMBIGUOUS_GESTURE, we risk that *nothing*
720                             // will happen in response to user input. To prevent this,
721                             // reschedule long press with a modified timeout.
722                             mHandler.removeMessages(LONG_PRESS);
723                             final long longPressTimeout = ViewConfiguration.getLongPressTimeout();
724                             mHandler.sendMessageAtTime(
725                                     mHandler.obtainMessage(
726                                             LONG_PRESS,
727                                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS,
728                                             0 /* arg2 */),
729                                     ev.getDownTime()
730                                         + (long) (longPressTimeout * mAmbiguousGestureMultiplier));
731                         }
732                         // Inhibit default scroll. If a gesture is ambiguous, we prevent scroll
733                         // until the gesture is resolved.
734                         // However, for safety, simply increase the touch slop in case the
735                         // classification is erroneous. Since the value is squared, multiply twice.
736                         slopSquare *= mAmbiguousGestureMultiplier * mAmbiguousGestureMultiplier;
737                     }
738 
739                     if (distance > slopSquare) {
740                         recordGestureClassification(
741                                 TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
742                         handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
743                         mLastFocusX = focusX;
744                         mLastFocusY = focusY;
745                         mAlwaysInTapRegion = false;
746                         mHandler.removeMessages(TAP);
747                         mHandler.removeMessages(SHOW_PRESS);
748                         mHandler.removeMessages(LONG_PRESS);
749                     }
750                     int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
751                     if (distance > doubleTapSlopSquare) {
752                         mAlwaysInBiggerTapRegion = false;
753                     }
754                 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
755                     recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SCROLL);
756                     handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
757                     mLastFocusX = focusX;
758                     mLastFocusY = focusY;
759                 }
760                 final boolean deepPress =
761                         motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
762                 if (deepPress && hasPendingLongPress) {
763                     mHandler.removeMessages(LONG_PRESS);
764                     mHandler.sendMessage(
765                             mHandler.obtainMessage(
766                                   LONG_PRESS,
767                                   TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS,
768                                   0 /* arg2 */));
769                 }
770                 break;
771 
772             case MotionEvent.ACTION_UP:
773                 mStillDown = false;
774                 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
775                 if (mIsDoubleTapping) {
776                     // Finally, give the up event of the double-tap
777                     recordGestureClassification(
778                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP);
779                     handled |= mDoubleTapListener.onDoubleTapEvent(ev);
780                 } else if (mInLongPress) {
781                     mHandler.removeMessages(TAP);
782                     mInLongPress = false;
783                 } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
784                     recordGestureClassification(
785                             TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
786                     handled = mListener.onSingleTapUp(ev);
787                     if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
788                         mDoubleTapListener.onSingleTapConfirmed(ev);
789                     }
790                 } else if (!mIgnoreNextUpEvent) {
791 
792                     // A fling must travel the minimum tap distance
793                     final VelocityTracker velocityTracker = mVelocityTracker;
794                     final int pointerId = ev.getPointerId(0);
795                     velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
796                     final float velocityY = velocityTracker.getYVelocity(pointerId);
797                     final float velocityX = velocityTracker.getXVelocity(pointerId);
798 
799                     if ((Math.abs(velocityY) > mMinimumFlingVelocity)
800                             || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
801                         handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
802                     }
803                 }
804                 if (mPreviousUpEvent != null) {
805                     mPreviousUpEvent.recycle();
806                 }
807                 // Hold the event we obtained above - listeners may have changed the original.
808                 mPreviousUpEvent = currentUpEvent;
809                 if (mVelocityTracker != null) {
810                     // This may have been cleared when we called out to the
811                     // application above.
812                     mVelocityTracker.recycle();
813                     mVelocityTracker = null;
814                 }
815                 mIsDoubleTapping = false;
816                 mDeferConfirmSingleTap = false;
817                 mIgnoreNextUpEvent = false;
818                 mHandler.removeMessages(SHOW_PRESS);
819                 mHandler.removeMessages(LONG_PRESS);
820                 break;
821 
822             case MotionEvent.ACTION_CANCEL:
823                 cancel();
824                 break;
825         }
826 
827         if (!handled && mInputEventConsistencyVerifier != null) {
828             mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
829         }
830         return handled;
831     }
832 
833     /**
834      * Analyzes the given generic motion event and if applicable triggers the
835      * appropriate callbacks on the {@link OnGestureListener} supplied.
836      *
837      * @param ev The current motion event.
838      * @return true if the {@link OnGestureListener} consumed the event,
839      *              else false.
840      */
onGenericMotionEvent(@onNull MotionEvent ev)841     public boolean onGenericMotionEvent(@NonNull MotionEvent ev) {
842         if (mInputEventConsistencyVerifier != null) {
843             mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
844         }
845 
846         final int actionButton = ev.getActionButton();
847         switch (ev.getActionMasked()) {
848             case MotionEvent.ACTION_BUTTON_PRESS:
849                 if (mContextClickListener != null && !mInContextClick && !mInLongPress
850                         && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
851                         || actionButton == MotionEvent.BUTTON_SECONDARY)) {
852                     if (mContextClickListener.onContextClick(ev)) {
853                         mInContextClick = true;
854                         mHandler.removeMessages(LONG_PRESS);
855                         mHandler.removeMessages(TAP);
856                         return true;
857                     }
858                 }
859                 break;
860 
861             case MotionEvent.ACTION_BUTTON_RELEASE:
862                 if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
863                         || actionButton == MotionEvent.BUTTON_SECONDARY)) {
864                     mInContextClick = false;
865                     mIgnoreNextUpEvent = true;
866                 }
867                 break;
868         }
869         return false;
870     }
871 
cancel()872     private void cancel() {
873         mHandler.removeMessages(SHOW_PRESS);
874         mHandler.removeMessages(LONG_PRESS);
875         mHandler.removeMessages(TAP);
876         if (mVelocityTracker != null) {
877             mVelocityTracker.recycle();
878             mVelocityTracker = null;
879         }
880         mIsDoubleTapping = false;
881         mStillDown = false;
882         mAlwaysInTapRegion = false;
883         mAlwaysInBiggerTapRegion = false;
884         mDeferConfirmSingleTap = false;
885         mInLongPress = false;
886         mInContextClick = false;
887         mIgnoreNextUpEvent = false;
888     }
889 
cancelTaps()890     private void cancelTaps() {
891         mHandler.removeMessages(SHOW_PRESS);
892         mHandler.removeMessages(LONG_PRESS);
893         mHandler.removeMessages(TAP);
894         mIsDoubleTapping = false;
895         mAlwaysInTapRegion = false;
896         mAlwaysInBiggerTapRegion = false;
897         mDeferConfirmSingleTap = false;
898         mInLongPress = false;
899         mInContextClick = false;
900         mIgnoreNextUpEvent = false;
901     }
902 
isConsideredDoubleTap(@onNull MotionEvent firstDown, @NonNull MotionEvent firstUp, @NonNull MotionEvent secondDown)903     private boolean isConsideredDoubleTap(@NonNull MotionEvent firstDown,
904             @NonNull MotionEvent firstUp, @NonNull MotionEvent secondDown) {
905         if (!mAlwaysInBiggerTapRegion) {
906             return false;
907         }
908 
909         final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
910         if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
911             return false;
912         }
913 
914         int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
915         int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
916         final boolean isGeneratedGesture =
917                 (firstDown.getFlags() & MotionEvent.FLAG_IS_GENERATED_GESTURE) != 0;
918         int slopSquare = isGeneratedGesture ? 0 : mDoubleTapSlopSquare;
919         return (deltaX * deltaX + deltaY * deltaY < slopSquare);
920     }
921 
dispatchLongPress()922     private void dispatchLongPress() {
923         mHandler.removeMessages(TAP);
924         mDeferConfirmSingleTap = false;
925         mInLongPress = true;
926         mListener.onLongPress(mCurrentDownEvent);
927     }
928 
recordGestureClassification(int classification)929     private void recordGestureClassification(int classification) {
930         if (mHasRecordedClassification
931                 || classification
932                     == TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION) {
933             // Only record the first classification for an event stream.
934             return;
935         }
936         if (mCurrentDownEvent == null || mCurrentMotionEvent == null) {
937             // If the complete event stream wasn't seen, don't record anything.
938             mHasRecordedClassification = true;
939             return;
940         }
941         FrameworkStatsLog.write(
942                 FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED,
943                 getClass().getName(),
944                 classification,
945                 (int) (SystemClock.uptimeMillis() - mCurrentMotionEvent.getDownTime()),
946                 (float) Math.hypot(mCurrentMotionEvent.getRawX() - mCurrentDownEvent.getRawX(),
947                                    mCurrentMotionEvent.getRawY() - mCurrentDownEvent.getRawY()));
948         mHasRecordedClassification = true;
949     }
950 }
951