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     private class GestureHandler extends Handler {
    249         GestureHandler() {
    250             super();
    251         }
    252 
    253         GestureHandler(Handler handler) {
    254             super(handler.getLooper());
    255         }
    256 
    257         @Override
    258         public void handleMessage(Message msg) {
    259             switch (msg.what) {
    260             case SHOW_PRESS:
    261                 mListener.onShowPress(mCurrentDownEvent);
    262                 break;
    263 
    264             case LONG_PRESS:
    265                 dispatchLongPress();
    266                 break;
    267 
    268             case TAP:
    269                 // If the user's finger is still down, do not count it as a tap
    270                 if (mDoubleTapListener != null && !mStillDown) {
    271                     mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
    272                 }
    273                 break;
    274 
    275             default:
    276                 throw new RuntimeException("Unknown message " + msg); //never
    277             }
    278         }
    279     }
    280 
    281     /**
    282      * Creates a GestureDetector with the supplied listener.
    283      * This variant of the constructor should be used from a non-UI thread
    284      * (as it allows specifying the Handler).
    285      *
    286      * @param listener the listener invoked for all the callbacks, this must
    287      * not be null.
    288      * @param handler the handler to use
    289      *
    290      * @throws NullPointerException if either {@code listener} or
    291      * {@code handler} is null.
    292      *
    293      * @deprecated Use {@link #GestureDetector(android.content.Context,
    294      *      android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
    295      */
    296     @Deprecated
    297     public GestureDetector(OnGestureListener listener, Handler handler) {
    298         this(null, listener, handler);
    299     }
    300 
    301     /**
    302      * Creates a GestureDetector with the supplied listener.
    303      * You may only use this constructor from a UI thread (this is the usual situation).
    304      * @see android.os.Handler#Handler()
    305      *
    306      * @param listener the listener invoked for all the callbacks, this must
    307      * not be null.
    308      *
    309      * @throws NullPointerException if {@code listener} is null.
    310      *
    311      * @deprecated Use {@link #GestureDetector(android.content.Context,
    312      *      android.view.GestureDetector.OnGestureListener)} instead.
    313      */
    314     @Deprecated
    315     public GestureDetector(OnGestureListener listener) {
    316         this(null, listener, null);
    317     }
    318 
    319     /**
    320      * Creates a GestureDetector with the supplied listener.
    321      * You may only use this constructor from a UI thread (this is the usual situation).
    322      * @see android.os.Handler#Handler()
    323      *
    324      * @param context the application's context
    325      * @param listener the listener invoked for all the callbacks, this must
    326      * not be null.
    327      *
    328      * @throws NullPointerException if {@code listener} is null.
    329      */
    330     public GestureDetector(Context context, OnGestureListener listener) {
    331         this(context, listener, null);
    332     }
    333 
    334     /**
    335      * Creates a GestureDetector with the supplied listener.
    336      * You may only use this constructor from a UI thread (this is the usual situation).
    337      * @see android.os.Handler#Handler()
    338      *
    339      * @param context the application's context
    340      * @param listener the listener invoked for all the callbacks, this must
    341      * not be null.
    342      * @param handler the handler to use
    343      *
    344      * @throws NullPointerException if {@code listener} is null.
    345      */
    346     public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
    347         this(context, listener, handler, context != null &&
    348                 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO);
    349     }
    350 
    351     /**
    352      * Creates a GestureDetector with the supplied listener.
    353      * You may only use this constructor from a UI thread (this is the usual situation).
    354      * @see android.os.Handler#Handler()
    355      *
    356      * @param context the application's context
    357      * @param listener the listener invoked for all the callbacks, this must
    358      * not be null.
    359      * @param handler the handler to use
    360      * @param ignoreMultitouch whether events involving more than one pointer should
    361      * be ignored.
    362      *
    363      * @throws NullPointerException if {@code listener} is null.
    364      */
    365     public GestureDetector(Context context, OnGestureListener listener, Handler handler,
    366             boolean ignoreMultitouch) {
    367         if (handler != null) {
    368             mHandler = new GestureHandler(handler);
    369         } else {
    370             mHandler = new GestureHandler();
    371         }
    372         mListener = listener;
    373         if (listener instanceof OnDoubleTapListener) {
    374             setOnDoubleTapListener((OnDoubleTapListener) listener);
    375         }
    376         init(context, ignoreMultitouch);
    377     }
    378 
    379     private void init(Context context, boolean ignoreMultitouch) {
    380         if (mListener == null) {
    381             throw new NullPointerException("OnGestureListener must not be null");
    382         }
    383         mIsLongpressEnabled = true;
    384         mIgnoreMultitouch = ignoreMultitouch;
    385 
    386         // Fallback to support pre-donuts releases
    387         int touchSlop, doubleTapSlop;
    388         if (context == null) {
    389             //noinspection deprecation
    390             touchSlop = ViewConfiguration.getTouchSlop();
    391             doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
    392             //noinspection deprecation
    393             mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
    394             mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
    395         } else {
    396             final ViewConfiguration configuration = ViewConfiguration.get(context);
    397             touchSlop = configuration.getScaledTouchSlop();
    398             doubleTapSlop = configuration.getScaledDoubleTapSlop();
    399             mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
    400             mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
    401         }
    402         mTouchSlopSquare = touchSlop * touchSlop;
    403         mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    404     }
    405 
    406     /**
    407      * Sets the listener which will be called for double-tap and related
    408      * gestures.
    409      *
    410      * @param onDoubleTapListener the listener invoked for all the callbacks, or
    411      *        null to stop listening for double-tap gestures.
    412      */
    413     public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
    414         mDoubleTapListener = onDoubleTapListener;
    415     }
    416 
    417     /**
    418      * Set whether longpress is enabled, if this is enabled when a user
    419      * presses and holds down you get a longpress event and nothing further.
    420      * If it's disabled the user can press and hold down and then later
    421      * moved their finger and you will get scroll events. By default
    422      * longpress is enabled.
    423      *
    424      * @param isLongpressEnabled whether longpress should be enabled.
    425      */
    426     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
    427         mIsLongpressEnabled = isLongpressEnabled;
    428     }
    429 
    430     /**
    431      * @return true if longpress is enabled, else false.
    432      */
    433     public boolean isLongpressEnabled() {
    434         return mIsLongpressEnabled;
    435     }
    436 
    437     /**
    438      * Analyzes the given motion event and if applicable triggers the
    439      * appropriate callbacks on the {@link OnGestureListener} supplied.
    440      *
    441      * @param ev The current motion event.
    442      * @return true if the {@link OnGestureListener} consumed the event,
    443      *              else false.
    444      */
    445     public boolean onTouchEvent(MotionEvent ev) {
    446         final int action = ev.getAction();
    447         final float y = ev.getY();
    448         final float x = ev.getX();
    449 
    450         if (mVelocityTracker == null) {
    451             mVelocityTracker = VelocityTracker.obtain();
    452         }
    453         mVelocityTracker.addMovement(ev);
    454 
    455         boolean handled = false;
    456 
    457         switch (action & MotionEvent.ACTION_MASK) {
    458         case MotionEvent.ACTION_POINTER_DOWN:
    459             if (mIgnoreMultitouch) {
    460                 // Multitouch event - abort.
    461                 cancel();
    462             }
    463             break;
    464 
    465         case MotionEvent.ACTION_POINTER_UP:
    466             // Ending a multitouch gesture and going back to 1 finger
    467             if (mIgnoreMultitouch && ev.getPointerCount() == 2) {
    468                 int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
    469                         >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0;
    470                 mLastMotionX = ev.getX(index);
    471                 mLastMotionY = ev.getY(index);
    472                 mVelocityTracker.recycle();
    473                 mVelocityTracker = VelocityTracker.obtain();
    474             }
    475             break;
    476 
    477         case MotionEvent.ACTION_DOWN:
    478             if (mDoubleTapListener != null) {
    479                 boolean hadTapMessage = mHandler.hasMessages(TAP);
    480                 if (hadTapMessage) mHandler.removeMessages(TAP);
    481                 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
    482                         isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
    483                     // This is a second tap
    484                     mIsDoubleTapping = true;
    485                     // Give a callback with the first tap of the double-tap
    486                     handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
    487                     // Give a callback with down event of the double-tap
    488                     handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    489                 } else {
    490                     // This is a first tap
    491                     mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
    492                 }
    493             }
    494 
    495             mLastMotionX = x;
    496             mLastMotionY = y;
    497             if (mCurrentDownEvent != null) {
    498                 mCurrentDownEvent.recycle();
    499             }
    500             mCurrentDownEvent = MotionEvent.obtain(ev);
    501             mAlwaysInTapRegion = true;
    502             mAlwaysInBiggerTapRegion = true;
    503             mStillDown = true;
    504             mInLongPress = false;
    505 
    506             if (mIsLongpressEnabled) {
    507                 mHandler.removeMessages(LONG_PRESS);
    508                 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
    509                         + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
    510             }
    511             mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
    512             handled |= mListener.onDown(ev);
    513             break;
    514 
    515         case MotionEvent.ACTION_MOVE:
    516             if (mInLongPress || (mIgnoreMultitouch && ev.getPointerCount() > 1)) {
    517                 break;
    518             }
    519             final float scrollX = mLastMotionX - x;
    520             final float scrollY = mLastMotionY - y;
    521             if (mIsDoubleTapping) {
    522                 // Give the move events of the double-tap
    523                 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    524             } else if (mAlwaysInTapRegion) {
    525                 final int deltaX = (int) (x - mCurrentDownEvent.getX());
    526                 final int deltaY = (int) (y - mCurrentDownEvent.getY());
    527                 int distance = (deltaX * deltaX) + (deltaY * deltaY);
    528                 if (distance > mTouchSlopSquare) {
    529                     handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    530                     mLastMotionX = x;
    531                     mLastMotionY = y;
    532                     mAlwaysInTapRegion = false;
    533                     mHandler.removeMessages(TAP);
    534                     mHandler.removeMessages(SHOW_PRESS);
    535                     mHandler.removeMessages(LONG_PRESS);
    536                 }
    537                 if (distance > mBiggerTouchSlopSquare) {
    538                     mAlwaysInBiggerTapRegion = false;
    539                 }
    540             } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
    541                 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    542                 mLastMotionX = x;
    543                 mLastMotionY = y;
    544             }
    545             break;
    546 
    547         case MotionEvent.ACTION_UP:
    548             mStillDown = false;
    549             MotionEvent currentUpEvent = MotionEvent.obtain(ev);
    550             if (mIsDoubleTapping) {
    551                 // Finally, give the up event of the double-tap
    552                 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    553             } else if (mInLongPress) {
    554                 mHandler.removeMessages(TAP);
    555                 mInLongPress = false;
    556             } else if (mAlwaysInTapRegion) {
    557                 handled = mListener.onSingleTapUp(ev);
    558             } else {
    559 
    560                 // A fling must travel the minimum tap distance
    561                 final VelocityTracker velocityTracker = mVelocityTracker;
    562                 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
    563                 final float velocityY = velocityTracker.getYVelocity();
    564                 final float velocityX = velocityTracker.getXVelocity();
    565 
    566                 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
    567                         || (Math.abs(velocityX) > mMinimumFlingVelocity)){
    568                     handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
    569                 }
    570             }
    571             if (mPreviousUpEvent != null) {
    572                 mPreviousUpEvent.recycle();
    573             }
    574             // Hold the event we obtained above - listeners may have changed the original.
    575             mPreviousUpEvent = currentUpEvent;
    576             mVelocityTracker.recycle();
    577             mVelocityTracker = null;
    578             mIsDoubleTapping = false;
    579             mHandler.removeMessages(SHOW_PRESS);
    580             mHandler.removeMessages(LONG_PRESS);
    581             break;
    582         case MotionEvent.ACTION_CANCEL:
    583             cancel();
    584         }
    585         return handled;
    586     }
    587 
    588     private void cancel() {
    589         mHandler.removeMessages(SHOW_PRESS);
    590         mHandler.removeMessages(LONG_PRESS);
    591         mHandler.removeMessages(TAP);
    592         mVelocityTracker.recycle();
    593         mVelocityTracker = null;
    594         mIsDoubleTapping = false;
    595         mStillDown = false;
    596         if (mInLongPress) {
    597             mInLongPress = false;
    598         }
    599     }
    600 
    601     private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
    602             MotionEvent secondDown) {
    603         if (!mAlwaysInBiggerTapRegion) {
    604             return false;
    605         }
    606 
    607         if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
    608             return false;
    609         }
    610 
    611         int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
    612         int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
    613         return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    614     }
    615 
    616     private void dispatchLongPress() {
    617         mHandler.removeMessages(TAP);
    618         mInLongPress = true;
    619         mListener.onLongPress(mCurrentDownEvent);
    620     }
    621 }
    622