Home | History | Annotate | Download | only in view
      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 
     17 package android.view;
     18 
     19 import android.content.Context;
     20 import android.os.Build;
     21 import android.os.Handler;
     22 import 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  */
     38 public 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