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