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