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