Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2010 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.util.DisplayMetrics;
     21 import android.util.FloatMath;
     22 import android.util.Log;
     23 
     24 /**
     25  * Detects transformation gestures involving more than one pointer ("multitouch")
     26  * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
     27  * callback will notify users when a particular gesture event has occurred.
     28  * This class should only be used with {@link MotionEvent}s reported via touch.
     29  *
     30  * To use this class:
     31  * <ul>
     32  *  <li>Create an instance of the {@code ScaleGestureDetector} for your
     33  *      {@link View}
     34  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
     35  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
     36  *          callback will be executed when the events occur.
     37  * </ul>
     38  */
     39 public class ScaleGestureDetector {
     40     private static final String TAG = "ScaleGestureDetector";
     41 
     42     /**
     43      * The listener for receiving notifications when gestures occur.
     44      * If you want to listen for all the different gestures then implement
     45      * this interface. If you only want to listen for a subset it might
     46      * be easier to extend {@link SimpleOnScaleGestureListener}.
     47      *
     48      * An application will receive events in the following order:
     49      * <ul>
     50      *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
     51      *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
     52      *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
     53      * </ul>
     54      */
     55     public interface OnScaleGestureListener {
     56         /**
     57          * Responds to scaling events for a gesture in progress.
     58          * Reported by pointer motion.
     59          *
     60          * @param detector The detector reporting the event - use this to
     61          *          retrieve extended info about event state.
     62          * @return Whether or not the detector should consider this event
     63          *          as handled. If an event was not handled, the detector
     64          *          will continue to accumulate movement until an event is
     65          *          handled. This can be useful if an application, for example,
     66          *          only wants to update scaling factors if the change is
     67          *          greater than 0.01.
     68          */
     69         public boolean onScale(ScaleGestureDetector detector);
     70 
     71         /**
     72          * Responds to the beginning of a scaling gesture. Reported by
     73          * new pointers going down.
     74          *
     75          * @param detector The detector reporting the event - use this to
     76          *          retrieve extended info about event state.
     77          * @return Whether or not the detector should continue recognizing
     78          *          this gesture. For example, if a gesture is beginning
     79          *          with a focal point outside of a region where it makes
     80          *          sense, onScaleBegin() may return false to ignore the
     81          *          rest of the gesture.
     82          */
     83         public boolean onScaleBegin(ScaleGestureDetector detector);
     84 
     85         /**
     86          * Responds to the end of a scale gesture. Reported by existing
     87          * pointers going up.
     88          *
     89          * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
     90          * and {@link ScaleGestureDetector#getFocusY()} will return the location
     91          * of the pointer remaining on the screen.
     92          *
     93          * @param detector The detector reporting the event - use this to
     94          *          retrieve extended info about event state.
     95          */
     96         public void onScaleEnd(ScaleGestureDetector detector);
     97     }
     98 
     99     /**
    100      * A convenience class to extend when you only want to listen for a subset
    101      * of scaling-related events. This implements all methods in
    102      * {@link OnScaleGestureListener} but does nothing.
    103      * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
    104      * {@code false} so that a subclass can retrieve the accumulated scale
    105      * factor in an overridden onScaleEnd.
    106      * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
    107      * {@code true}.
    108      */
    109     public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
    110 
    111         public boolean onScale(ScaleGestureDetector detector) {
    112             return false;
    113         }
    114 
    115         public boolean onScaleBegin(ScaleGestureDetector detector) {
    116             return true;
    117         }
    118 
    119         public void onScaleEnd(ScaleGestureDetector detector) {
    120             // Intentionally empty
    121         }
    122     }
    123 
    124     /**
    125      * This value is the threshold ratio between our previous combined pressure
    126      * and the current combined pressure. We will only fire an onScale event if
    127      * the computed ratio between the current and previous event pressures is
    128      * greater than this value. When pressure decreases rapidly between events
    129      * the position values can often be imprecise, as it usually indicates
    130      * that the user is in the process of lifting a pointer off of the device.
    131      * Its value was tuned experimentally.
    132      */
    133     private static final float PRESSURE_THRESHOLD = 0.67f;
    134 
    135     private final Context mContext;
    136     private final OnScaleGestureListener mListener;
    137     private boolean mGestureInProgress;
    138 
    139     private MotionEvent mPrevEvent;
    140     private MotionEvent mCurrEvent;
    141 
    142     private float mFocusX;
    143     private float mFocusY;
    144     private float mPrevFingerDiffX;
    145     private float mPrevFingerDiffY;
    146     private float mCurrFingerDiffX;
    147     private float mCurrFingerDiffY;
    148     private float mCurrLen;
    149     private float mPrevLen;
    150     private float mScaleFactor;
    151     private float mCurrPressure;
    152     private float mPrevPressure;
    153     private long mTimeDelta;
    154 
    155     private boolean mInvalidGesture;
    156 
    157     // Pointer IDs currently responsible for the two fingers controlling the gesture
    158     private int mActiveId0;
    159     private int mActiveId1;
    160     private boolean mActive0MostRecent;
    161 
    162     /**
    163      * Consistency verifier for debugging purposes.
    164      */
    165     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
    166             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
    167                     new InputEventConsistencyVerifier(this, 0) : null;
    168 
    169     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
    170         mContext = context;
    171         mListener = listener;
    172     }
    173 
    174     public boolean onTouchEvent(MotionEvent event) {
    175         if (mInputEventConsistencyVerifier != null) {
    176             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    177         }
    178 
    179         final int action = event.getActionMasked();
    180 
    181         if (action == MotionEvent.ACTION_DOWN) {
    182             reset(); // Start fresh
    183         }
    184 
    185         boolean handled = true;
    186         if (mInvalidGesture) {
    187             handled = false;
    188         } else if (!mGestureInProgress) {
    189             switch (action) {
    190                 case MotionEvent.ACTION_DOWN: {
    191                     mActiveId0 = event.getPointerId(0);
    192                     mActive0MostRecent = true;
    193                 }
    194                 break;
    195 
    196                 case MotionEvent.ACTION_UP:
    197                     reset();
    198                     break;
    199 
    200                 case MotionEvent.ACTION_POINTER_DOWN: {
    201                     // We have a new multi-finger gesture
    202                     if (mPrevEvent != null) mPrevEvent.recycle();
    203                     mPrevEvent = MotionEvent.obtain(event);
    204                     mTimeDelta = 0;
    205 
    206                     int index1 = event.getActionIndex();
    207                     int index0 = event.findPointerIndex(mActiveId0);
    208                     mActiveId1 = event.getPointerId(index1);
    209                     if (index0 < 0 || index0 == index1) {
    210                         // Probably someone sending us a broken event stream.
    211                         index0 = findNewActiveIndex(event, mActiveId1, -1);
    212                         mActiveId0 = event.getPointerId(index0);
    213                     }
    214                     mActive0MostRecent = false;
    215 
    216                     setContext(event);
    217 
    218                     mGestureInProgress = mListener.onScaleBegin(this);
    219                     break;
    220                 }
    221             }
    222         } else {
    223             // Transform gesture in progress - attempt to handle it
    224             switch (action) {
    225                 case MotionEvent.ACTION_POINTER_DOWN: {
    226                     // End the old gesture and begin a new one with the most recent two fingers.
    227                     mListener.onScaleEnd(this);
    228                     final int oldActive0 = mActiveId0;
    229                     final int oldActive1 = mActiveId1;
    230                     reset();
    231 
    232                     mPrevEvent = MotionEvent.obtain(event);
    233                     mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1;
    234                     mActiveId1 = event.getPointerId(event.getActionIndex());
    235                     mActive0MostRecent = false;
    236 
    237                     int index0 = event.findPointerIndex(mActiveId0);
    238                     if (index0 < 0 || mActiveId0 == mActiveId1) {
    239                         // Probably someone sending us a broken event stream.
    240                         Log.e(TAG, "Got " + MotionEvent.actionToString(action) +
    241                                 " with bad state while a gesture was in progress. " +
    242                                 "Did you forget to pass an event to " +
    243                                 "ScaleGestureDetector#onTouchEvent?");
    244                         index0 = findNewActiveIndex(event, mActiveId1, -1);
    245                         mActiveId0 = event.getPointerId(index0);
    246                     }
    247 
    248                     setContext(event);
    249 
    250                     mGestureInProgress = mListener.onScaleBegin(this);
    251                 }
    252                 break;
    253 
    254                 case MotionEvent.ACTION_POINTER_UP: {
    255                     final int pointerCount = event.getPointerCount();
    256                     final int actionIndex = event.getActionIndex();
    257                     final int actionId = event.getPointerId(actionIndex);
    258 
    259                     boolean gestureEnded = false;
    260                     if (pointerCount > 2) {
    261                         if (actionId == mActiveId0) {
    262                             final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex);
    263                             if (newIndex >= 0) {
    264                                 mListener.onScaleEnd(this);
    265                                 mActiveId0 = event.getPointerId(newIndex);
    266                                 mActive0MostRecent = true;
    267                                 mPrevEvent = MotionEvent.obtain(event);
    268                                 setContext(event);
    269                                 mGestureInProgress = mListener.onScaleBegin(this);
    270                             } else {
    271                                 gestureEnded = true;
    272                             }
    273                         } else if (actionId == mActiveId1) {
    274                             final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex);
    275                             if (newIndex >= 0) {
    276                                 mListener.onScaleEnd(this);
    277                                 mActiveId1 = event.getPointerId(newIndex);
    278                                 mActive0MostRecent = false;
    279                                 mPrevEvent = MotionEvent.obtain(event);
    280                                 setContext(event);
    281                                 mGestureInProgress = mListener.onScaleBegin(this);
    282                             } else {
    283                                 gestureEnded = true;
    284                             }
    285                         }
    286                         mPrevEvent.recycle();
    287                         mPrevEvent = MotionEvent.obtain(event);
    288                         setContext(event);
    289                     } else {
    290                         gestureEnded = true;
    291                     }
    292 
    293                     if (gestureEnded) {
    294                         // Gesture ended
    295                         setContext(event);
    296 
    297                         // Set focus point to the remaining finger
    298                         final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0;
    299                         final int index = event.findPointerIndex(activeId);
    300                         mFocusX = event.getX(index);
    301                         mFocusY = event.getY(index);
    302 
    303                         mListener.onScaleEnd(this);
    304                         reset();
    305                         mActiveId0 = activeId;
    306                         mActive0MostRecent = true;
    307                     }
    308                 }
    309                 break;
    310 
    311                 case MotionEvent.ACTION_CANCEL:
    312                     mListener.onScaleEnd(this);
    313                     reset();
    314                     break;
    315 
    316                 case MotionEvent.ACTION_UP:
    317                     reset();
    318                     break;
    319 
    320                 case MotionEvent.ACTION_MOVE: {
    321                     setContext(event);
    322 
    323                     // Only accept the event if our relative pressure is within
    324                     // a certain limit - this can help filter shaky data as a
    325                     // finger is lifted.
    326                     if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
    327                         final boolean updatePrevious = mListener.onScale(this);
    328 
    329                         if (updatePrevious) {
    330                             mPrevEvent.recycle();
    331                             mPrevEvent = MotionEvent.obtain(event);
    332                         }
    333                     }
    334                 }
    335                 break;
    336             }
    337         }
    338 
    339         if (!handled && mInputEventConsistencyVerifier != null) {
    340             mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    341         }
    342         return handled;
    343     }
    344 
    345     private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex) {
    346         final int pointerCount = ev.getPointerCount();
    347 
    348         // It's ok if this isn't found and returns -1, it simply won't match.
    349         final int otherActiveIndex = ev.findPointerIndex(otherActiveId);
    350 
    351         // Pick a new id and update tracking state.
    352         for (int i = 0; i < pointerCount; i++) {
    353             if (i != removedPointerIndex && i != otherActiveIndex) {
    354                 return i;
    355             }
    356         }
    357         return -1;
    358     }
    359 
    360     private void setContext(MotionEvent curr) {
    361         if (mCurrEvent != null) {
    362             mCurrEvent.recycle();
    363         }
    364         mCurrEvent = MotionEvent.obtain(curr);
    365 
    366         mCurrLen = -1;
    367         mPrevLen = -1;
    368         mScaleFactor = -1;
    369 
    370         final MotionEvent prev = mPrevEvent;
    371 
    372         final int prevIndex0 = prev.findPointerIndex(mActiveId0);
    373         final int prevIndex1 = prev.findPointerIndex(mActiveId1);
    374         final int currIndex0 = curr.findPointerIndex(mActiveId0);
    375         final int currIndex1 = curr.findPointerIndex(mActiveId1);
    376 
    377         if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) {
    378             mInvalidGesture = true;
    379             Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable());
    380             if (mGestureInProgress) {
    381                 mListener.onScaleEnd(this);
    382             }
    383             return;
    384         }
    385 
    386         final float px0 = prev.getX(prevIndex0);
    387         final float py0 = prev.getY(prevIndex0);
    388         final float px1 = prev.getX(prevIndex1);
    389         final float py1 = prev.getY(prevIndex1);
    390         final float cx0 = curr.getX(currIndex0);
    391         final float cy0 = curr.getY(currIndex0);
    392         final float cx1 = curr.getX(currIndex1);
    393         final float cy1 = curr.getY(currIndex1);
    394 
    395         final float pvx = px1 - px0;
    396         final float pvy = py1 - py0;
    397         final float cvx = cx1 - cx0;
    398         final float cvy = cy1 - cy0;
    399         mPrevFingerDiffX = pvx;
    400         mPrevFingerDiffY = pvy;
    401         mCurrFingerDiffX = cvx;
    402         mCurrFingerDiffY = cvy;
    403 
    404         mFocusX = cx0 + cvx * 0.5f;
    405         mFocusY = cy0 + cvy * 0.5f;
    406         mTimeDelta = curr.getEventTime() - prev.getEventTime();
    407         mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1);
    408         mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1);
    409     }
    410 
    411     private void reset() {
    412         if (mPrevEvent != null) {
    413             mPrevEvent.recycle();
    414             mPrevEvent = null;
    415         }
    416         if (mCurrEvent != null) {
    417             mCurrEvent.recycle();
    418             mCurrEvent = null;
    419         }
    420         mGestureInProgress = false;
    421         mActiveId0 = -1;
    422         mActiveId1 = -1;
    423         mInvalidGesture = false;
    424     }
    425 
    426     /**
    427      * Returns {@code true} if a two-finger scale gesture is in progress.
    428      * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
    429      */
    430     public boolean isInProgress() {
    431         return mGestureInProgress;
    432     }
    433 
    434     /**
    435      * Get the X coordinate of the current gesture's focal point.
    436      * If a gesture is in progress, the focal point is directly between
    437      * the two pointers forming the gesture.
    438      * If a gesture is ending, the focal point is the location of the
    439      * remaining pointer on the screen.
    440      * If {@link #isInProgress()} would return false, the result of this
    441      * function is undefined.
    442      *
    443      * @return X coordinate of the focal point in pixels.
    444      */
    445     public float getFocusX() {
    446         return mFocusX;
    447     }
    448 
    449     /**
    450      * Get the Y coordinate of the current gesture's focal point.
    451      * If a gesture is in progress, the focal point is directly between
    452      * the two pointers forming the gesture.
    453      * If a gesture is ending, the focal point is the location of the
    454      * remaining pointer on the screen.
    455      * If {@link #isInProgress()} would return false, the result of this
    456      * function is undefined.
    457      *
    458      * @return Y coordinate of the focal point in pixels.
    459      */
    460     public float getFocusY() {
    461         return mFocusY;
    462     }
    463 
    464     /**
    465      * Return the current distance between the two pointers forming the
    466      * gesture in progress.
    467      *
    468      * @return Distance between pointers in pixels.
    469      */
    470     public float getCurrentSpan() {
    471         if (mCurrLen == -1) {
    472             final float cvx = mCurrFingerDiffX;
    473             final float cvy = mCurrFingerDiffY;
    474             mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
    475         }
    476         return mCurrLen;
    477     }
    478 
    479     /**
    480      * Return the current x distance between the two pointers forming the
    481      * gesture in progress.
    482      *
    483      * @return Distance between pointers in pixels.
    484      */
    485     public float getCurrentSpanX() {
    486         return mCurrFingerDiffX;
    487     }
    488 
    489     /**
    490      * Return the current y distance between the two pointers forming the
    491      * gesture in progress.
    492      *
    493      * @return Distance between pointers in pixels.
    494      */
    495     public float getCurrentSpanY() {
    496         return mCurrFingerDiffY;
    497     }
    498 
    499     /**
    500      * Return the previous distance between the two pointers forming the
    501      * gesture in progress.
    502      *
    503      * @return Previous distance between pointers in pixels.
    504      */
    505     public float getPreviousSpan() {
    506         if (mPrevLen == -1) {
    507             final float pvx = mPrevFingerDiffX;
    508             final float pvy = mPrevFingerDiffY;
    509             mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
    510         }
    511         return mPrevLen;
    512     }
    513 
    514     /**
    515      * Return the previous x distance between the two pointers forming the
    516      * gesture in progress.
    517      *
    518      * @return Previous distance between pointers in pixels.
    519      */
    520     public float getPreviousSpanX() {
    521         return mPrevFingerDiffX;
    522     }
    523 
    524     /**
    525      * Return the previous y distance between the two pointers forming the
    526      * gesture in progress.
    527      *
    528      * @return Previous distance between pointers in pixels.
    529      */
    530     public float getPreviousSpanY() {
    531         return mPrevFingerDiffY;
    532     }
    533 
    534     /**
    535      * Return the scaling factor from the previous scale event to the current
    536      * event. This value is defined as
    537      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
    538      *
    539      * @return The current scaling factor.
    540      */
    541     public float getScaleFactor() {
    542         if (mScaleFactor == -1) {
    543             mScaleFactor = getCurrentSpan() / getPreviousSpan();
    544         }
    545         return mScaleFactor;
    546     }
    547 
    548     /**
    549      * Return the time difference in milliseconds between the previous
    550      * accepted scaling event and the current scaling event.
    551      *
    552      * @return Time difference since the last scaling event in milliseconds.
    553      */
    554     public long getTimeDelta() {
    555         return mTimeDelta;
    556     }
    557 
    558     /**
    559      * Return the event time of the current event being processed.
    560      *
    561      * @return Current event time in milliseconds.
    562      */
    563     public long getEventTime() {
    564         return mCurrEvent.getEventTime();
    565     }
    566 }
    567