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
506        // Determine focal point
507        float sumX = 0, sumY = 0;
508        final int count = ev.getPointerCount();
509        for (int i = 0; i < count; i++) {
510            if (skipIndex == i) continue;
511            sumX += ev.getX(i);
512            sumY += ev.getY(i);
513        }
514        final int div = pointerUp ? count - 1 : count;
515        final float focusX = sumX / div;
516        final float focusY = sumY / div;
517
518        boolean handled = false;
519
520        switch (action & MotionEvent.ACTION_MASK) {
521        case MotionEvent.ACTION_POINTER_DOWN:
522            mDownFocusX = mLastFocusX = focusX;
523            mDownFocusY = mLastFocusY = focusY;
524            // Cancel long press and taps
525            cancelTaps();
526            break;
527
528        case MotionEvent.ACTION_POINTER_UP:
529            mDownFocusX = mLastFocusX = focusX;
530            mDownFocusY = mLastFocusY = focusY;
531
532            // Check the dot product of current velocities.
533            // If the pointer that left was opposing another velocity vector, clear.
534            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
535            final int upIndex = ev.getActionIndex();
536            final int id1 = ev.getPointerId(upIndex);
537            final float x1 = mVelocityTracker.getXVelocity(id1);
538            final float y1 = mVelocityTracker.getYVelocity(id1);
539            for (int i = 0; i < count; i++) {
540                if (i == upIndex) continue;
541
542                final int id2 = ev.getPointerId(i);
543                final float x = x1 * mVelocityTracker.getXVelocity(id2);
544                final float y = y1 * mVelocityTracker.getYVelocity(id2);
545
546                final float dot = x + y;
547                if (dot < 0) {
548                    mVelocityTracker.clear();
549                    break;
550                }
551            }
552            break;
553
554        case MotionEvent.ACTION_DOWN:
555            if (mDoubleTapListener != null) {
556                boolean hadTapMessage = mHandler.hasMessages(TAP);
557                if (hadTapMessage) mHandler.removeMessages(TAP);
558                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
559                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
560                    // This is a second tap
561                    mIsDoubleTapping = true;
562                    // Give a callback with the first tap of the double-tap
563                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
564                    // Give a callback with down event of the double-tap
565                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
566                } else {
567                    // This is a first tap
568                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
569                }
570            }
571
572            mDownFocusX = mLastFocusX = focusX;
573            mDownFocusY = mLastFocusY = focusY;
574            if (mCurrentDownEvent != null) {
575                mCurrentDownEvent.recycle();
576            }
577            mCurrentDownEvent = MotionEvent.obtain(ev);
578            mAlwaysInTapRegion = true;
579            mAlwaysInBiggerTapRegion = true;
580            mStillDown = true;
581            mInLongPress = false;
582            mDeferConfirmSingleTap = false;
583
584            if (mIsLongpressEnabled) {
585                mHandler.removeMessages(LONG_PRESS);
586                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
587                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
588            }
589            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
590            handled |= mListener.onDown(ev);
591            break;
592
593        case MotionEvent.ACTION_MOVE:
594            if (mInLongPress || mInContextClick) {
595                break;
596            }
597            final float scrollX = mLastFocusX - focusX;
598            final float scrollY = mLastFocusY - focusY;
599            if (mIsDoubleTapping) {
600                // Give the move events of the double-tap
601                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
602            } else if (mAlwaysInTapRegion) {
603                final int deltaX = (int) (focusX - mDownFocusX);
604                final int deltaY = (int) (focusY - mDownFocusY);
605                int distance = (deltaX * deltaX) + (deltaY * deltaY);
606                if (distance > mTouchSlopSquare) {
607                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
608                    mLastFocusX = focusX;
609                    mLastFocusY = focusY;
610                    mAlwaysInTapRegion = false;
611                    mHandler.removeMessages(TAP);
612                    mHandler.removeMessages(SHOW_PRESS);
613                    mHandler.removeMessages(LONG_PRESS);
614                }
615                if (distance > mDoubleTapTouchSlopSquare) {
616                    mAlwaysInBiggerTapRegion = false;
617                }
618            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
619                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
620                mLastFocusX = focusX;
621                mLastFocusY = focusY;
622            }
623            break;
624
625        case MotionEvent.ACTION_UP:
626            mStillDown = false;
627            MotionEvent currentUpEvent = MotionEvent.obtain(ev);
628            if (mIsDoubleTapping) {
629                // Finally, give the up event of the double-tap
630                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
631            } else if (mInLongPress) {
632                mHandler.removeMessages(TAP);
633                mInLongPress = false;
634            } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
635                handled = mListener.onSingleTapUp(ev);
636                if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
637                    mDoubleTapListener.onSingleTapConfirmed(ev);
638                }
639            } else if (!mIgnoreNextUpEvent) {
640
641                // A fling must travel the minimum tap distance
642                final VelocityTracker velocityTracker = mVelocityTracker;
643                final int pointerId = ev.getPointerId(0);
644                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
645                final float velocityY = velocityTracker.getYVelocity(pointerId);
646                final float velocityX = velocityTracker.getXVelocity(pointerId);
647
648                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
649                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
650                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
651                }
652            }
653            if (mPreviousUpEvent != null) {
654                mPreviousUpEvent.recycle();
655            }
656            // Hold the event we obtained above - listeners may have changed the original.
657            mPreviousUpEvent = currentUpEvent;
658            if (mVelocityTracker != null) {
659                // This may have been cleared when we called out to the
660                // application above.
661                mVelocityTracker.recycle();
662                mVelocityTracker = null;
663            }
664            mIsDoubleTapping = false;
665            mDeferConfirmSingleTap = false;
666            mIgnoreNextUpEvent = false;
667            mHandler.removeMessages(SHOW_PRESS);
668            mHandler.removeMessages(LONG_PRESS);
669            break;
670
671        case MotionEvent.ACTION_CANCEL:
672            cancel();
673            break;
674        }
675
676        if (!handled && mInputEventConsistencyVerifier != null) {
677            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
678        }
679        return handled;
680    }
681
682    /**
683     * Analyzes the given generic motion event and if applicable triggers the
684     * appropriate callbacks on the {@link OnGestureListener} supplied.
685     *
686     * @param ev The current motion event.
687     * @return true if the {@link OnGestureListener} consumed the event,
688     *              else false.
689     */
690    public boolean onGenericMotionEvent(MotionEvent ev) {
691        if (mInputEventConsistencyVerifier != null) {
692            mInputEventConsistencyVerifier.onGenericMotionEvent(ev, 0);
693        }
694
695        final int actionButton = ev.getActionButton();
696        switch (ev.getActionMasked()) {
697            case MotionEvent.ACTION_BUTTON_PRESS:
698                if (mContextClickListener != null && !mInContextClick && !mInLongPress
699                        && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
700                        || actionButton == MotionEvent.BUTTON_SECONDARY)) {
701                    if (mContextClickListener.onContextClick(ev)) {
702                        mInContextClick = true;
703                        mHandler.removeMessages(LONG_PRESS);
704                        mHandler.removeMessages(TAP);
705                        return true;
706                    }
707                }
708                break;
709
710            case MotionEvent.ACTION_BUTTON_RELEASE:
711                if (mInContextClick && (actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
712                        || actionButton == MotionEvent.BUTTON_SECONDARY)) {
713                    mInContextClick = false;
714                    mIgnoreNextUpEvent = true;
715                }
716                break;
717        }
718        return false;
719    }
720
721    private void cancel() {
722        mHandler.removeMessages(SHOW_PRESS);
723        mHandler.removeMessages(LONG_PRESS);
724        mHandler.removeMessages(TAP);
725        mVelocityTracker.recycle();
726        mVelocityTracker = null;
727        mIsDoubleTapping = false;
728        mStillDown = false;
729        mAlwaysInTapRegion = false;
730        mAlwaysInBiggerTapRegion = false;
731        mDeferConfirmSingleTap = false;
732        mInLongPress = false;
733        mInContextClick = false;
734        mIgnoreNextUpEvent = false;
735    }
736
737    private void cancelTaps() {
738        mHandler.removeMessages(SHOW_PRESS);
739        mHandler.removeMessages(LONG_PRESS);
740        mHandler.removeMessages(TAP);
741        mIsDoubleTapping = false;
742        mAlwaysInTapRegion = false;
743        mAlwaysInBiggerTapRegion = false;
744        mDeferConfirmSingleTap = false;
745        mInLongPress = false;
746        mInContextClick = false;
747        mIgnoreNextUpEvent = false;
748    }
749
750    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
751            MotionEvent secondDown) {
752        if (!mAlwaysInBiggerTapRegion) {
753            return false;
754        }
755
756        final long deltaTime = secondDown.getEventTime() - firstUp.getEventTime();
757        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
758            return false;
759        }
760
761        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
762        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
763        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
764    }
765
766    private void dispatchLongPress() {
767        mHandler.removeMessages(TAP);
768        mDeferConfirmSingleTap = false;
769        mInLongPress = true;
770        mListener.onLongPress(mCurrentDownEvent);
771    }
772}
773