Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright 2018 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 androidx.core.view;
     18 
     19 import android.content.Context;
     20 import android.os.Build;
     21 import android.os.Handler;
     22 import android.os.Message;
     23 import android.view.GestureDetector;
     24 import android.view.GestureDetector.OnDoubleTapListener;
     25 import android.view.GestureDetector.OnGestureListener;
     26 import android.view.MotionEvent;
     27 import android.view.VelocityTracker;
     28 import android.view.View;
     29 import android.view.ViewConfiguration;
     30 
     31 /**
     32  * Detects various gestures and events using the supplied {@link MotionEvent}s.
     33  * The {@link OnGestureListener} callback will notify users when a particular
     34  * motion event has occurred. This class should only be used with {@link MotionEvent}s
     35  * reported via touch (don't use for trackball events).
     36  *
     37  * <p>This compatibility implementation of the framework's GestureDetector guarantees
     38  * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.</p>
     39  *
     40  * To use this class:
     41  * <ul>
     42  *  <li>Create an instance of the {@code GestureDetectorCompat} for your {@link View}
     43  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
     44  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
     45  *          will be executed when the events occur.
     46  * </ul>
     47  */
     48 public final class GestureDetectorCompat {
     49     interface GestureDetectorCompatImpl {
     50         boolean isLongpressEnabled();
     51         boolean onTouchEvent(MotionEvent ev);
     52         void setIsLongpressEnabled(boolean enabled);
     53         void setOnDoubleTapListener(OnDoubleTapListener listener);
     54     }
     55 
     56     static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl {
     57         private int mTouchSlopSquare;
     58         private int mDoubleTapSlopSquare;
     59         private int mMinimumFlingVelocity;
     60         private int mMaximumFlingVelocity;
     61 
     62         private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
     63         private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
     64         private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
     65 
     66         // constants for Message.what used by GestureHandler below
     67         private static final int SHOW_PRESS = 1;
     68         private static final int LONG_PRESS = 2;
     69         private static final int TAP = 3;
     70 
     71         private final Handler mHandler;
     72         final OnGestureListener mListener;
     73         OnDoubleTapListener mDoubleTapListener;
     74 
     75         boolean mStillDown;
     76         boolean mDeferConfirmSingleTap;
     77         private boolean mInLongPress;
     78         private boolean mAlwaysInTapRegion;
     79         private boolean mAlwaysInBiggerTapRegion;
     80 
     81         MotionEvent mCurrentDownEvent;
     82         private MotionEvent mPreviousUpEvent;
     83 
     84         /**
     85          * True when the user is still touching for the second tap (down, move, and
     86          * up events). Can only be true if there is a double tap listener attached.
     87          */
     88         private boolean mIsDoubleTapping;
     89 
     90         private float mLastFocusX;
     91         private float mLastFocusY;
     92         private float mDownFocusX;
     93         private float mDownFocusY;
     94 
     95         private boolean mIsLongpressEnabled;
     96 
     97         /**
     98          * Determines speed during touch scrolling
     99          */
    100         private VelocityTracker mVelocityTracker;
    101 
    102         private class GestureHandler extends Handler {
    103             GestureHandler() {
    104                 super();
    105             }
    106 
    107             GestureHandler(Handler handler) {
    108                 super(handler.getLooper());
    109             }
    110 
    111             @Override
    112             public void handleMessage(Message msg) {
    113                 switch (msg.what) {
    114                 case SHOW_PRESS:
    115                     mListener.onShowPress(mCurrentDownEvent);
    116                     break;
    117 
    118                 case LONG_PRESS:
    119                     dispatchLongPress();
    120                     break;
    121 
    122                 case TAP:
    123                     // If the user's finger is still down, do not count it as a tap
    124                     if (mDoubleTapListener != null) {
    125                         if (!mStillDown) {
    126                             mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
    127                         } else {
    128                             mDeferConfirmSingleTap = true;
    129                         }
    130                     }
    131                     break;
    132 
    133                 default:
    134                     throw new RuntimeException("Unknown message " + msg); //never
    135                 }
    136             }
    137         }
    138 
    139         /**
    140          * Creates a GestureDetector with the supplied listener.
    141          * You may only use this constructor from a UI thread (this is the usual situation).
    142          * @see android.os.Handler#Handler()
    143          *
    144          * @param context the application's context
    145          * @param listener the listener invoked for all the callbacks, this must
    146          * not be null.
    147          * @param handler the handler to use
    148          *
    149          * @throws NullPointerException if {@code listener} is null.
    150          */
    151         GestureDetectorCompatImplBase(Context context, OnGestureListener listener,
    152                 Handler handler) {
    153             if (handler != null) {
    154                 mHandler = new GestureHandler(handler);
    155             } else {
    156                 mHandler = new GestureHandler();
    157             }
    158             mListener = listener;
    159             if (listener instanceof OnDoubleTapListener) {
    160                 setOnDoubleTapListener((OnDoubleTapListener) listener);
    161             }
    162             init(context);
    163         }
    164 
    165         private void init(Context context) {
    166             if (context == null) {
    167                 throw new IllegalArgumentException("Context must not be null");
    168             }
    169             if (mListener == null) {
    170                 throw new IllegalArgumentException("OnGestureListener must not be null");
    171             }
    172             mIsLongpressEnabled = true;
    173 
    174             final ViewConfiguration configuration = ViewConfiguration.get(context);
    175             final int touchSlop = configuration.getScaledTouchSlop();
    176             final int doubleTapSlop = configuration.getScaledDoubleTapSlop();
    177             mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
    178             mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
    179 
    180             mTouchSlopSquare = touchSlop * touchSlop;
    181             mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    182         }
    183 
    184         /**
    185          * Sets the listener which will be called for double-tap and related
    186          * gestures.
    187          *
    188          * @param onDoubleTapListener the listener invoked for all the callbacks, or
    189          *        null to stop listening for double-tap gestures.
    190          */
    191         @Override
    192         public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
    193             mDoubleTapListener = onDoubleTapListener;
    194         }
    195 
    196         /**
    197          * Set whether longpress is enabled, if this is enabled when a user
    198          * presses and holds down you get a longpress event and nothing further.
    199          * If it's disabled the user can press and hold down and then later
    200          * moved their finger and you will get scroll events. By default
    201          * longpress is enabled.
    202          *
    203          * @param isLongpressEnabled whether longpress should be enabled.
    204          */
    205         @Override
    206         public void setIsLongpressEnabled(boolean isLongpressEnabled) {
    207             mIsLongpressEnabled = isLongpressEnabled;
    208         }
    209 
    210         /**
    211          * @return true if longpress is enabled, else false.
    212          */
    213         @Override
    214         public boolean isLongpressEnabled() {
    215             return mIsLongpressEnabled;
    216         }
    217 
    218         /**
    219          * Analyzes the given motion event and if applicable triggers the
    220          * appropriate callbacks on the {@link OnGestureListener} supplied.
    221          *
    222          * @param ev The current motion event.
    223          * @return true if the {@link OnGestureListener} consumed the event,
    224          *              else false.
    225          */
    226         @Override
    227         public boolean onTouchEvent(MotionEvent ev) {
    228             final int action = ev.getAction();
    229 
    230             if (mVelocityTracker == null) {
    231                 mVelocityTracker = VelocityTracker.obtain();
    232             }
    233             mVelocityTracker.addMovement(ev);
    234 
    235             final boolean pointerUp =
    236                     (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
    237             final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
    238 
    239             // Determine focal point
    240             float sumX = 0, sumY = 0;
    241             final int count = ev.getPointerCount();
    242             for (int i = 0; i < count; i++) {
    243                 if (skipIndex == i) continue;
    244                 sumX += ev.getX(i);
    245                 sumY += ev.getY(i);
    246             }
    247             final int div = pointerUp ? count - 1 : count;
    248             final float focusX = sumX / div;
    249             final float focusY = sumY / div;
    250 
    251             boolean handled = false;
    252 
    253             switch (action & MotionEvent.ACTION_MASK) {
    254                 case MotionEvent.ACTION_POINTER_DOWN:
    255                     mDownFocusX = mLastFocusX = focusX;
    256                     mDownFocusY = mLastFocusY = focusY;
    257                     // Cancel long press and taps
    258                     cancelTaps();
    259                     break;
    260 
    261                 case MotionEvent.ACTION_POINTER_UP:
    262                     mDownFocusX = mLastFocusX = focusX;
    263                     mDownFocusY = mLastFocusY = focusY;
    264 
    265                     // Check the dot product of current velocities.
    266                     // If the pointer that left was opposing another velocity vector, clear.
    267                     mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
    268                     final int upIndex = ev.getActionIndex();
    269                     final int id1 = ev.getPointerId(upIndex);
    270                     final float x1 = mVelocityTracker.getXVelocity(id1);
    271                     final float y1 = mVelocityTracker.getYVelocity(id1);
    272                     for (int i = 0; i < count; i++) {
    273                         if (i == upIndex) continue;
    274 
    275                         final int id2 = ev.getPointerId(i);
    276                         final float x = x1 * mVelocityTracker.getXVelocity(id2);
    277                         final float y = y1 * mVelocityTracker.getYVelocity(id2);
    278 
    279                         final float dot = x + y;
    280                         if (dot < 0) {
    281                             mVelocityTracker.clear();
    282                             break;
    283                         }
    284                     }
    285                     break;
    286 
    287                 case MotionEvent.ACTION_DOWN:
    288                     if (mDoubleTapListener != null) {
    289                         boolean hadTapMessage = mHandler.hasMessages(TAP);
    290                         if (hadTapMessage) mHandler.removeMessages(TAP);
    291                         if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null)
    292                                 && hadTapMessage && isConsideredDoubleTap(
    293                                         mCurrentDownEvent, mPreviousUpEvent, ev)) {
    294                             // This is a second tap
    295                             mIsDoubleTapping = true;
    296                             // Give a callback with the first tap of the double-tap
    297                             handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
    298                             // Give a callback with down event of the double-tap
    299                             handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    300                         } else {
    301                             // This is a first tap
    302                             mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
    303                         }
    304                     }
    305 
    306                     mDownFocusX = mLastFocusX = focusX;
    307                     mDownFocusY = mLastFocusY = focusY;
    308                     if (mCurrentDownEvent != null) {
    309                         mCurrentDownEvent.recycle();
    310                     }
    311                     mCurrentDownEvent = MotionEvent.obtain(ev);
    312                     mAlwaysInTapRegion = true;
    313                     mAlwaysInBiggerTapRegion = true;
    314                     mStillDown = true;
    315                     mInLongPress = false;
    316                     mDeferConfirmSingleTap = false;
    317 
    318                     if (mIsLongpressEnabled) {
    319                         mHandler.removeMessages(LONG_PRESS);
    320                         mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
    321                                 + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
    322                     }
    323                     mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
    324                             mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
    325                     handled |= mListener.onDown(ev);
    326                     break;
    327 
    328                 case MotionEvent.ACTION_MOVE:
    329                     if (mInLongPress) {
    330                         break;
    331                     }
    332                     final float scrollX = mLastFocusX - focusX;
    333                     final float scrollY = mLastFocusY - focusY;
    334                     if (mIsDoubleTapping) {
    335                         // Give the move events of the double-tap
    336                         handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    337                     } else if (mAlwaysInTapRegion) {
    338                         final int deltaX = (int) (focusX - mDownFocusX);
    339                         final int deltaY = (int) (focusY - mDownFocusY);
    340                         int distance = (deltaX * deltaX) + (deltaY * deltaY);
    341                         if (distance > mTouchSlopSquare) {
    342                             handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    343                             mLastFocusX = focusX;
    344                             mLastFocusY = focusY;
    345                             mAlwaysInTapRegion = false;
    346                             mHandler.removeMessages(TAP);
    347                             mHandler.removeMessages(SHOW_PRESS);
    348                             mHandler.removeMessages(LONG_PRESS);
    349                         }
    350                         if (distance > mTouchSlopSquare) {
    351                             mAlwaysInBiggerTapRegion = false;
    352                         }
    353                     } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
    354                         handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
    355                         mLastFocusX = focusX;
    356                         mLastFocusY = focusY;
    357                     }
    358                     break;
    359 
    360                 case MotionEvent.ACTION_UP:
    361                     mStillDown = false;
    362                     MotionEvent currentUpEvent = MotionEvent.obtain(ev);
    363                     if (mIsDoubleTapping) {
    364                         // Finally, give the up event of the double-tap
    365                         handled |= mDoubleTapListener.onDoubleTapEvent(ev);
    366                     } else if (mInLongPress) {
    367                         mHandler.removeMessages(TAP);
    368                         mInLongPress = false;
    369                     } else if (mAlwaysInTapRegion) {
    370                         handled = mListener.onSingleTapUp(ev);
    371                         if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
    372                             mDoubleTapListener.onSingleTapConfirmed(ev);
    373                         }
    374                     } else {
    375                         // A fling must travel the minimum tap distance
    376                         final VelocityTracker velocityTracker = mVelocityTracker;
    377                         final int pointerId = ev.getPointerId(0);
    378                         velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
    379                         final float velocityY = velocityTracker.getYVelocity(pointerId);
    380                         final float velocityX = velocityTracker.getXVelocity(pointerId);
    381 
    382                         if ((Math.abs(velocityY) > mMinimumFlingVelocity)
    383                                 || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
    384                             handled = mListener.onFling(
    385                                     mCurrentDownEvent, ev, velocityX, velocityY);
    386                         }
    387                     }
    388                     if (mPreviousUpEvent != null) {
    389                         mPreviousUpEvent.recycle();
    390                     }
    391                     // Hold the event we obtained above - listeners may have changed the original.
    392                     mPreviousUpEvent = currentUpEvent;
    393                     if (mVelocityTracker != null) {
    394                         // This may have been cleared when we called out to the
    395                         // application above.
    396                         mVelocityTracker.recycle();
    397                         mVelocityTracker = null;
    398                     }
    399                     mIsDoubleTapping = false;
    400                     mDeferConfirmSingleTap = false;
    401                     mHandler.removeMessages(SHOW_PRESS);
    402                     mHandler.removeMessages(LONG_PRESS);
    403                     break;
    404 
    405                 case MotionEvent.ACTION_CANCEL:
    406                     cancel();
    407                     break;
    408             }
    409 
    410             return handled;
    411         }
    412 
    413         private void cancel() {
    414             mHandler.removeMessages(SHOW_PRESS);
    415             mHandler.removeMessages(LONG_PRESS);
    416             mHandler.removeMessages(TAP);
    417             mVelocityTracker.recycle();
    418             mVelocityTracker = null;
    419             mIsDoubleTapping = false;
    420             mStillDown = false;
    421             mAlwaysInTapRegion = false;
    422             mAlwaysInBiggerTapRegion = false;
    423             mDeferConfirmSingleTap = false;
    424             if (mInLongPress) {
    425                 mInLongPress = false;
    426             }
    427         }
    428 
    429         private void cancelTaps() {
    430             mHandler.removeMessages(SHOW_PRESS);
    431             mHandler.removeMessages(LONG_PRESS);
    432             mHandler.removeMessages(TAP);
    433             mIsDoubleTapping = false;
    434             mAlwaysInTapRegion = false;
    435             mAlwaysInBiggerTapRegion = false;
    436             mDeferConfirmSingleTap = false;
    437             if (mInLongPress) {
    438                 mInLongPress = false;
    439             }
    440         }
    441 
    442         private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
    443                 MotionEvent secondDown) {
    444             if (!mAlwaysInBiggerTapRegion) {
    445                 return false;
    446             }
    447 
    448             if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
    449                 return false;
    450             }
    451 
    452             int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
    453             int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
    454             return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
    455         }
    456 
    457         void dispatchLongPress() {
    458             mHandler.removeMessages(TAP);
    459             mDeferConfirmSingleTap = false;
    460             mInLongPress = true;
    461             mListener.onLongPress(mCurrentDownEvent);
    462         }
    463     }
    464 
    465     static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl {
    466         private final GestureDetector mDetector;
    467 
    468         GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener,
    469                 Handler handler) {
    470             mDetector = new GestureDetector(context, listener, handler);
    471         }
    472 
    473         @Override
    474         public boolean isLongpressEnabled() {
    475             return mDetector.isLongpressEnabled();
    476         }
    477 
    478         @Override
    479         public boolean onTouchEvent(MotionEvent ev) {
    480             return mDetector.onTouchEvent(ev);
    481         }
    482 
    483         @Override
    484         public void setIsLongpressEnabled(boolean enabled) {
    485             mDetector.setIsLongpressEnabled(enabled);
    486         }
    487 
    488         @Override
    489         public void setOnDoubleTapListener(OnDoubleTapListener listener) {
    490             mDetector.setOnDoubleTapListener(listener);
    491         }
    492     }
    493 
    494     private final GestureDetectorCompatImpl mImpl;
    495 
    496     /**
    497      * Creates a GestureDetectorCompat with the supplied listener.
    498      * As usual, you may only use this constructor from a UI thread.
    499      * @see android.os.Handler#Handler()
    500      *
    501      * @param context the application's context
    502      * @param listener the listener invoked for all the callbacks, this must
    503      * not be null.
    504      */
    505     public GestureDetectorCompat(Context context, OnGestureListener listener) {
    506         this(context, listener, null);
    507     }
    508 
    509     /**
    510      * Creates a GestureDetectorCompat with the supplied listener.
    511      * As usual, you may only use this constructor from a UI thread.
    512      * @see android.os.Handler#Handler()
    513      *
    514      * @param context the application's context
    515      * @param listener the listener invoked for all the callbacks, this must
    516      * not be null.
    517      * @param handler the handler that will be used for posting deferred messages
    518      */
    519     public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) {
    520         if (Build.VERSION.SDK_INT > 17) {
    521             mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler);
    522         } else {
    523             mImpl = new GestureDetectorCompatImplBase(context, listener, handler);
    524         }
    525     }
    526 
    527     /**
    528      * @return true if longpress is enabled, else false.
    529      */
    530     public boolean isLongpressEnabled() {
    531         return mImpl.isLongpressEnabled();
    532     }
    533 
    534     /**
    535      * Analyzes the given motion event and if applicable triggers the
    536      * appropriate callbacks on the {@link OnGestureListener} supplied.
    537      *
    538      * @param event The current motion event.
    539      * @return true if the {@link OnGestureListener} consumed the event,
    540      *              else false.
    541      */
    542     public boolean onTouchEvent(MotionEvent event) {
    543         return mImpl.onTouchEvent(event);
    544     }
    545 
    546     /**
    547      * Set whether longpress is enabled, if this is enabled when a user
    548      * presses and holds down you get a longpress event and nothing further.
    549      * If it's disabled the user can press and hold down and then later
    550      * moved their finger and you will get scroll events. By default
    551      * longpress is enabled.
    552      *
    553      * @param enabled whether longpress should be enabled.
    554      */
    555     public void setIsLongpressEnabled(boolean enabled) {
    556         mImpl.setIsLongpressEnabled(enabled);
    557     }
    558 
    559     /**
    560      * Sets the listener which will be called for double-tap and related
    561      * gestures.
    562      *
    563      * @param listener the listener invoked for all the callbacks, or
    564      *        null to stop listening for double-tap gestures.
    565      */
    566     public void setOnDoubleTapListener(OnDoubleTapListener listener) {
    567         mImpl.setOnDoubleTapListener(listener);
    568     }
    569 }
    570