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