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
17package android.view;
18
19import android.content.Context;
20import android.os.Handler;
21import 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 */
40public 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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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
178        public boolean onSingleTapUp(MotionEvent e) {
179            return false;
180        }
181
182        public void onLongPress(MotionEvent e) {
183        }
184
185        public boolean onScroll(MotionEvent e1, MotionEvent e2,
186                float distanceX, float distanceY) {
187            return false;
188        }
189
190        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
191                float velocityY) {
192            return false;
193        }
194
195        public void onShowPress(MotionEvent e) {
196        }
197
198        public boolean onDown(MotionEvent e) {
199            return false;
200        }
201
202        public boolean onDoubleTap(MotionEvent e) {
203            return false;
204        }
205
206        public boolean onDoubleTapEvent(MotionEvent e) {
207            return false;
208        }
209
210        public boolean onSingleTapConfirmed(MotionEvent e) {
211            return false;
212        }
213
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 {
277        GestureHandler() {
278            super();
279        }
280
281        GestureHandler(Handler handler) {
282            super(handler.getLooper());
283        }
284
285        @Override
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
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
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     */
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     */
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     */
407    public GestureDetector(Context context, OnGestureListener listener, Handler handler,
408            boolean unused) {
409        this(context, listener, handler);
410    }
411
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     */
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     */
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     */
471    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
472        mIsLongpressEnabled = isLongpressEnabled;
473    }
474
475    /**
476     * @return true if longpress is enabled, else false.
477     */
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     */
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     */
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
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
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
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
775    private void dispatchLongPress() {
776        mHandler.removeMessages(TAP);
777        mDeferConfirmSingleTap = false;
778        mInLongPress = true;
779        mListener.onLongPress(mCurrentDownEvent);
780    }
781}
782