19ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch/*
29ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * Copyright (C) 2012 The Android Open Source Project
39ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch *
49ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * Licensed under the Apache License, Version 2.0 (the "License");
59ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * you may not use this file except in compliance with the License.
69ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * You may obtain a copy of the License at
79ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch *
89ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch *      http://www.apache.org/licenses/LICENSE-2.0
9ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch *
109ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * Unless required by applicable law or agreed to in writing, software
11ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch * distributed under the License is distributed on an "AS IS" BASIS,
129ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles) * See the License for the specific language governing permissions and
14ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch * limitations under the License.
159ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch */
169ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
179ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochpackage android.support.v4.view;
189ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
19d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)import android.content.Context;
200f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)import android.os.Build;
219ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.os.Handler;
229ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.os.Message;
239ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.view.GestureDetector;
249ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.view.GestureDetector.OnDoubleTapListener;
25ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdochimport android.view.GestureDetector.OnGestureListener;
269ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.view.MotionEvent;
279ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdochimport android.view.VelocityTracker;
28ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdochimport android.view.View;
29ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdochimport android.view.ViewConfiguration;
300f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
310f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)/**
320f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * Detects various gestures and events using the supplied {@link MotionEvent}s.
330f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * The {@link OnGestureListener} callback will notify users when a particular
340f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * motion event has occurred. This class should only be used with {@link MotionEvent}s
350f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * reported via touch (don't use for trackball events).
360f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *
370f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * <p>This compatibility implementation of the framework's GestureDetector guarantees
380f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.</p>
390f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *
400f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * To use this class:
410f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * <ul>
420f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *  <li>Create an instance of the {@code GestureDetectorCompat} for your {@link View}
430f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
440f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
450f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) *          will be executed when the events occur.
460f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) * </ul>
470f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles) */
480f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)public class GestureDetectorCompat {
490f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)    interface GestureDetectorCompatImpl {
500f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        boolean isLongpressEnabled();
510f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        boolean onTouchEvent(MotionEvent ev);
520f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        void setIsLongpressEnabled(boolean enabled);
530f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        void setOnDoubleTapListener(OnDoubleTapListener listener);
540f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)    }
550f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
560f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)    static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl {
570f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private int mTouchSlopSquare;
580f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private int mDoubleTapSlopSquare;
590f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private int mMinimumFlingVelocity;
60ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        private int mMaximumFlingVelocity;
61ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
624e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
634e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
644e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
650f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
660f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        // constants for Message.what used by GestureHandler below
670f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private static final int SHOW_PRESS = 1;
680f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private static final int LONG_PRESS = 2;
694e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private static final int TAP = 3;
700f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
710f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private final Handler mHandler;
720f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private final OnGestureListener mListener;
730f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private OnDoubleTapListener mDoubleTapListener;
740f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
750f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mStillDown;
760f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mDeferConfirmSingleTap;
770f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mInLongPress;
780f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mAlwaysInTapRegion;
790f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mAlwaysInBiggerTapRegion;
800f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
810f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private MotionEvent mCurrentDownEvent;
820f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private MotionEvent mPreviousUpEvent;
830f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
840f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        /**
850f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * True when the user is still touching for the second tap (down, move, and
864e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)         * up events). Can only be true if there is a double tap listener attached.
874e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)         */
880f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        private boolean mIsDoubleTapping;
890f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)
904e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private float mLastFocusX;
914e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private float mLastFocusY;
92ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        private float mDownFocusX;
93ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        private float mDownFocusY;
94ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
95ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        private boolean mIsLongpressEnabled;
969ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
979ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch        /**
989ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch         * Determines speed during touch scrolling
999ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch         */
1009ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch        private VelocityTracker mVelocityTracker;
1019ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
1029ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch        private class GestureHandler extends Handler {
1039ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            GestureHandler() {
1049ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                super();
1059ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            }
1069ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
1079ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            GestureHandler(Handler handler) {
1089ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                super(handler.getLooper());
1099ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            }
1109ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
1119ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            @Override
1129ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch            public void handleMessage(Message msg) {
1139ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                switch (msg.what) {
1149ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                case SHOW_PRESS:
1159ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    mListener.onShowPress(mCurrentDownEvent);
1169ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    break;
1179ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
1189ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                case LONG_PRESS:
1199ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    dispatchLongPress();
1209ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    break;
1219ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch
1229ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                case TAP:
1239ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    // If the user's finger is still down, do not count it as a tap
1249ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                    if (mDoubleTapListener != null) {
1259ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch                        if (!mStillDown) {
126ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                            mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
127ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                        } else {
128ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                            mDeferConfirmSingleTap = true;
1290f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                        }
1300f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                    }
131ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                    break;
132ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
133ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                default:
1340f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                    throw new RuntimeException("Unknown message " + msg); //never
135ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch                }
1364e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)            }
137ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        }
138ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch
139ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch        /**
1402385ea399aae016c0806a4f9ef3c9cfe3d2a39dfBen Murdoch         * Creates a GestureDetector with the supplied listener.
141ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch         * You may only use this constructor from a UI thread (this is the usual situation).
142ca12bfac764ba476d6cd062bf1dde12cc64c3f40Ben Murdoch         * @see android.os.Handler#Handler()
1430f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         *
1440f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * @param context the application's context
1450f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * @param listener the listener invoked for all the callbacks, this must
1460f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * not be null.
1470f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * @param handler the handler to use
1480f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         *
1490f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         * @throws NullPointerException if {@code listener} is null.
1500f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)         */
1510f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        public GestureDetectorCompatImplBase(Context context, OnGestureListener listener,
1520f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                Handler handler) {
1530f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)            if (handler != null) {
1540f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                mHandler = new GestureHandler(handler);
1550f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)            } else {
1560f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)                mHandler = new GestureHandler();
1570f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)            }
1580f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)            mListener = listener;
1594e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)            if (listener instanceof OnDoubleTapListener) {
1604e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)                setOnDoubleTapListener((OnDoubleTapListener) listener);
1614e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)            }
1624e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)            init(context);
1630f1bc08d4cfcc34181b0b5cbf065c40f687bf740Torne (Richard Coles)        }
1644e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)
1654e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)        private void init(Context context) {
1664e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)            if (context == null) {
1674e180b6a0b4720a9b8e9e959a882386f690f08ffTorne (Richard Coles)                throw new IllegalArgumentException("Context must not be null");
16868043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)            }
16968043e1e95eeb07d5cae7aca370b26518b0867d6Torne (Richard Coles)            if (mListener == null) {
170d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)                throw new IllegalArgumentException("OnGestureListener must not be null");
171d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            }
172d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            mIsLongpressEnabled = true;
173d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
174d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            final ViewConfiguration configuration = ViewConfiguration.get(context);
175d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            final int touchSlop = configuration.getScaledTouchSlop();
176d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
177d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
178d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
179d0247b1b59f9c528cb6df88b4f2b9afaf80d181eTorne (Richard Coles)
180f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)            mTouchSlopSquare = touchSlop * touchSlop;
181f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)            mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
182f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        }
183f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)
184f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)        /**
185f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         * Sets the listener which will be called for double-tap and related
186f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         * gestures.
187f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         *
188f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         * @param onDoubleTapListener the listener invoked for all the callbacks, or
189f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         *        null to stop listening for double-tap gestures.
190f2477e01787aa58f445919b809d89e252beef54fTorne (Richard Coles)         */
1919ab5563a3196760eb381d102cbb2bc0f7abc6a50Ben Murdoch        public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
192            mDoubleTapListener = onDoubleTapListener;
193        }
194
195        /**
196         * Set whether longpress is enabled, if this is enabled when a user
197         * presses and holds down you get a longpress event and nothing further.
198         * If it's disabled the user can press and hold down and then later
199         * moved their finger and you will get scroll events. By default
200         * longpress is enabled.
201         *
202         * @param isLongpressEnabled whether longpress should be enabled.
203         */
204        public void setIsLongpressEnabled(boolean isLongpressEnabled) {
205            mIsLongpressEnabled = isLongpressEnabled;
206        }
207
208        /**
209         * @return true if longpress is enabled, else false.
210         */
211        public boolean isLongpressEnabled() {
212            return mIsLongpressEnabled;
213        }
214
215        /**
216         * Analyzes the given motion event and if applicable triggers the
217         * appropriate callbacks on the {@link OnGestureListener} supplied.
218         *
219         * @param ev The current motion event.
220         * @return true if the {@link OnGestureListener} consumed the event,
221         *              else false.
222         */
223        public boolean onTouchEvent(MotionEvent ev) {
224            final int action = ev.getAction();
225
226            if (mVelocityTracker == null) {
227                mVelocityTracker = VelocityTracker.obtain();
228            }
229            mVelocityTracker.addMovement(ev);
230
231            final boolean pointerUp =
232                    (action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP;
233            final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1;
234
235            // Determine focal point
236            float sumX = 0, sumY = 0;
237            final int count = MotionEventCompat.getPointerCount(ev);
238            for (int i = 0; i < count; i++) {
239                if (skipIndex == i) continue;
240                sumX += MotionEventCompat.getX(ev, i);
241                sumY += MotionEventCompat.getY(ev, i);
242            }
243            final int div = pointerUp ? count - 1 : count;
244            final float focusX = sumX / div;
245            final float focusY = sumY / div;
246
247            boolean handled = false;
248
249            switch (action & MotionEventCompat.ACTION_MASK) {
250            case MotionEventCompat.ACTION_POINTER_DOWN:
251                mDownFocusX = mLastFocusX = focusX;
252                mDownFocusY = mLastFocusY = focusY;
253                // Cancel long press and taps
254                cancelTaps();
255                break;
256
257            case MotionEventCompat.ACTION_POINTER_UP:
258                mDownFocusX = mLastFocusX = focusX;
259                mDownFocusY = mLastFocusY = focusY;
260
261                // Check the dot product of current velocities.
262                // If the pointer that left was opposing another velocity vector, clear.
263                mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
264                final int upIndex = MotionEventCompat.getActionIndex(ev);
265                final int id1 = MotionEventCompat.getPointerId(ev, upIndex);
266                final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1);
267                final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1);
268                for (int i = 0; i < count; i++) {
269                    if (i == upIndex) continue;
270
271                    final int id2 = MotionEventCompat.getPointerId(ev, i);
272                    final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2);
273                    final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2);
274
275                    final float dot = x + y;
276                    if (dot < 0) {
277                        mVelocityTracker.clear();
278                        break;
279                    }
280                }
281                break;
282
283            case MotionEvent.ACTION_DOWN:
284                if (mDoubleTapListener != null) {
285                    boolean hadTapMessage = mHandler.hasMessages(TAP);
286                    if (hadTapMessage) mHandler.removeMessages(TAP);
287                    if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
288                            isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
289                        // This is a second tap
290                        mIsDoubleTapping = true;
291                        // Give a callback with the first tap of the double-tap
292                        handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
293                        // Give a callback with down event of the double-tap
294                        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
295                    } else {
296                        // This is a first tap
297                        mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
298                    }
299                }
300
301                mDownFocusX = mLastFocusX = focusX;
302                mDownFocusY = mLastFocusY = focusY;
303                if (mCurrentDownEvent != null) {
304                    mCurrentDownEvent.recycle();
305                }
306                mCurrentDownEvent = MotionEvent.obtain(ev);
307                mAlwaysInTapRegion = true;
308                mAlwaysInBiggerTapRegion = true;
309                mStillDown = true;
310                mInLongPress = false;
311                mDeferConfirmSingleTap = false;
312
313                if (mIsLongpressEnabled) {
314                    mHandler.removeMessages(LONG_PRESS);
315                    mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
316                            + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
317                }
318                mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
319                handled |= mListener.onDown(ev);
320                break;
321
322            case MotionEvent.ACTION_MOVE:
323                if (mInLongPress) {
324                    break;
325                }
326                final float scrollX = mLastFocusX - focusX;
327                final float scrollY = mLastFocusY - focusY;
328                if (mIsDoubleTapping) {
329                    // Give the move events of the double-tap
330                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
331                } else if (mAlwaysInTapRegion) {
332                    final int deltaX = (int) (focusX - mDownFocusX);
333                    final int deltaY = (int) (focusY - mDownFocusY);
334                    int distance = (deltaX * deltaX) + (deltaY * deltaY);
335                    if (distance > mTouchSlopSquare) {
336                        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
337                        mLastFocusX = focusX;
338                        mLastFocusY = focusY;
339                        mAlwaysInTapRegion = false;
340                        mHandler.removeMessages(TAP);
341                        mHandler.removeMessages(SHOW_PRESS);
342                        mHandler.removeMessages(LONG_PRESS);
343                    }
344                    if (distance > mTouchSlopSquare) {
345                        mAlwaysInBiggerTapRegion = false;
346                    }
347                } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
348                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
349                    mLastFocusX = focusX;
350                    mLastFocusY = focusY;
351                }
352                break;
353
354            case MotionEvent.ACTION_UP:
355                mStillDown = false;
356                MotionEvent currentUpEvent = MotionEvent.obtain(ev);
357                if (mIsDoubleTapping) {
358                    // Finally, give the up event of the double-tap
359                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
360                } else if (mInLongPress) {
361                    mHandler.removeMessages(TAP);
362                    mInLongPress = false;
363                } else if (mAlwaysInTapRegion) {
364                    handled = mListener.onSingleTapUp(ev);
365                    if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
366                        mDoubleTapListener.onSingleTapConfirmed(ev);
367                    }
368                } else {
369                    // A fling must travel the minimum tap distance
370                    final VelocityTracker velocityTracker = mVelocityTracker;
371                    final int pointerId = MotionEventCompat.getPointerId(ev, 0);
372                    velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
373                    final float velocityY = VelocityTrackerCompat.getYVelocity(
374                            velocityTracker, pointerId);
375                    final float velocityX = VelocityTrackerCompat.getXVelocity(
376                            velocityTracker, pointerId);
377
378                    if ((Math.abs(velocityY) > mMinimumFlingVelocity)
379                            || (Math.abs(velocityX) > mMinimumFlingVelocity)){
380                        handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
381                    }
382                }
383                if (mPreviousUpEvent != null) {
384                    mPreviousUpEvent.recycle();
385                }
386                // Hold the event we obtained above - listeners may have changed the original.
387                mPreviousUpEvent = currentUpEvent;
388                if (mVelocityTracker != null) {
389                    // This may have been cleared when we called out to the
390                    // application above.
391                    mVelocityTracker.recycle();
392                    mVelocityTracker = null;
393                }
394                mIsDoubleTapping = false;
395                mDeferConfirmSingleTap = false;
396                mHandler.removeMessages(SHOW_PRESS);
397                mHandler.removeMessages(LONG_PRESS);
398                break;
399
400            case MotionEvent.ACTION_CANCEL:
401                cancel();
402                break;
403            }
404
405            return handled;
406        }
407
408        private void cancel() {
409            mHandler.removeMessages(SHOW_PRESS);
410            mHandler.removeMessages(LONG_PRESS);
411            mHandler.removeMessages(TAP);
412            mVelocityTracker.recycle();
413            mVelocityTracker = null;
414            mIsDoubleTapping = false;
415            mStillDown = false;
416            mAlwaysInTapRegion = false;
417            mAlwaysInBiggerTapRegion = false;
418            mDeferConfirmSingleTap = false;
419            if (mInLongPress) {
420                mInLongPress = false;
421            }
422        }
423
424        private void cancelTaps() {
425            mHandler.removeMessages(SHOW_PRESS);
426            mHandler.removeMessages(LONG_PRESS);
427            mHandler.removeMessages(TAP);
428            mIsDoubleTapping = false;
429            mAlwaysInTapRegion = false;
430            mAlwaysInBiggerTapRegion = false;
431            mDeferConfirmSingleTap = false;
432            if (mInLongPress) {
433                mInLongPress = false;
434            }
435        }
436
437        private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
438                MotionEvent secondDown) {
439            if (!mAlwaysInBiggerTapRegion) {
440                return false;
441            }
442
443            if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
444                return false;
445            }
446
447            int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
448            int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
449            return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
450        }
451
452        private void dispatchLongPress() {
453            mHandler.removeMessages(TAP);
454            mDeferConfirmSingleTap = false;
455            mInLongPress = true;
456            mListener.onLongPress(mCurrentDownEvent);
457        }
458    }
459
460    static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl {
461        private final GestureDetector mDetector;
462
463        public GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener,
464                Handler handler) {
465            mDetector = new GestureDetector(context, listener, handler);
466        }
467
468        @Override
469        public boolean isLongpressEnabled() {
470            return mDetector.isLongpressEnabled();
471        }
472
473        @Override
474        public boolean onTouchEvent(MotionEvent ev) {
475            return mDetector.onTouchEvent(ev);
476        }
477
478        @Override
479        public void setIsLongpressEnabled(boolean enabled) {
480            mDetector.setIsLongpressEnabled(enabled);
481        }
482
483        @Override
484        public void setOnDoubleTapListener(OnDoubleTapListener listener) {
485            mDetector.setOnDoubleTapListener(listener);
486        }
487    }
488
489    private final GestureDetectorCompatImpl mImpl;
490
491    /**
492     * Creates a GestureDetectorCompat with the supplied listener.
493     * As usual, you may only use this constructor from a UI thread.
494     * @see android.os.Handler#Handler()
495     *
496     * @param context the application's context
497     * @param listener the listener invoked for all the callbacks, this must
498     * not be null.
499     */
500    public GestureDetectorCompat(Context context, OnGestureListener listener) {
501        this(context, listener, null);
502    }
503
504    /**
505     * Creates a GestureDetectorCompat with the supplied listener.
506     * As usual, you may only use this constructor from a UI thread.
507     * @see android.os.Handler#Handler()
508     *
509     * @param context the application's context
510     * @param listener the listener invoked for all the callbacks, this must
511     * not be null.
512     * @param handler the handler that will be used for posting deferred messages
513     */
514    public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) {
515        if (Build.VERSION.SDK_INT > 17) {
516            mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler);
517        } else {
518            mImpl = new GestureDetectorCompatImplBase(context, listener, handler);
519        }
520    }
521
522    /**
523     * @return true if longpress is enabled, else false.
524     */
525    public boolean isLongpressEnabled() {
526        return mImpl.isLongpressEnabled();
527    }
528
529    /**
530     * Analyzes the given motion event and if applicable triggers the
531     * appropriate callbacks on the {@link OnGestureListener} supplied.
532     *
533     * @param event The current motion event.
534     * @return true if the {@link OnGestureListener} consumed the event,
535     *              else false.
536     */
537    public boolean onTouchEvent(MotionEvent event) {
538        return mImpl.onTouchEvent(event);
539    }
540
541    /**
542     * Set whether longpress is enabled, if this is enabled when a user
543     * presses and holds down you get a longpress event and nothing further.
544     * If it's disabled the user can press and hold down and then later
545     * moved their finger and you will get scroll events. By default
546     * longpress is enabled.
547     *
548     * @param enabled whether longpress should be enabled.
549     */
550    public void setIsLongpressEnabled(boolean enabled) {
551        mImpl.setIsLongpressEnabled(enabled);
552    }
553
554    /**
555     * Sets the listener which will be called for double-tap and related
556     * gestures.
557     *
558     * @param listener the listener invoked for all the callbacks, or
559     *        null to stop listening for double-tap gestures.
560     */
561    public void setOnDoubleTapListener(OnDoubleTapListener listener) {
562        mImpl.setOnDoubleTapListener(listener);
563    }
564}
565