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