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