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