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.content.res.Resources;
     21 import android.os.Build;
     22 import android.os.Handler;
     23 import android.os.SystemClock;
     24 import android.util.FloatMath;
     25 
     26 /**
     27  * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
     28  * The {@link OnScaleGestureListener} callback will notify users when a particular
     29  * gesture event has occurred.
     30  *
     31  * This class should only be used with {@link MotionEvent}s reported via touch.
     32  *
     33  * To use this class:
     34  * <ul>
     35  *  <li>Create an instance of the {@code ScaleGestureDetector} for your
     36  *      {@link View}
     37  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
     38  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
     39  *          callback will be executed when the events occur.
     40  * </ul>
     41  */
     42 public class ScaleGestureDetector {
     43     private static final String TAG = "ScaleGestureDetector";
     44 
     45     /**
     46      * The listener for receiving notifications when gestures occur.
     47      * If you want to listen for all the different gestures then implement
     48      * this interface. If you only want to listen for a subset it might
     49      * be easier to extend {@link SimpleOnScaleGestureListener}.
     50      *
     51      * An application will receive events in the following order:
     52      * <ul>
     53      *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
     54      *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
     55      *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
     56      * </ul>
     57      */
     58     public interface OnScaleGestureListener {
     59         /**
     60          * Responds to scaling events for a gesture in progress.
     61          * Reported by pointer motion.
     62          *
     63          * @param detector The detector reporting the event - use this to
     64          *          retrieve extended info about event state.
     65          * @return Whether or not the detector should consider this event
     66          *          as handled. If an event was not handled, the detector
     67          *          will continue to accumulate movement until an event is
     68          *          handled. This can be useful if an application, for example,
     69          *          only wants to update scaling factors if the change is
     70          *          greater than 0.01.
     71          */
     72         public boolean onScale(ScaleGestureDetector detector);
     73 
     74         /**
     75          * Responds to the beginning of a scaling gesture. Reported by
     76          * new pointers going down.
     77          *
     78          * @param detector The detector reporting the event - use this to
     79          *          retrieve extended info about event state.
     80          * @return Whether or not the detector should continue recognizing
     81          *          this gesture. For example, if a gesture is beginning
     82          *          with a focal point outside of a region where it makes
     83          *          sense, onScaleBegin() may return false to ignore the
     84          *          rest of the gesture.
     85          */
     86         public boolean onScaleBegin(ScaleGestureDetector detector);
     87 
     88         /**
     89          * Responds to the end of a scale gesture. Reported by existing
     90          * pointers going up.
     91          *
     92          * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
     93          * and {@link ScaleGestureDetector#getFocusY()} will return focal point
     94          * of the pointers remaining on the screen.
     95          *
     96          * @param detector The detector reporting the event - use this to
     97          *          retrieve extended info about event state.
     98          */
     99         public void onScaleEnd(ScaleGestureDetector detector);
    100     }
    101 
    102     /**
    103      * A convenience class to extend when you only want to listen for a subset
    104      * of scaling-related events. This implements all methods in
    105      * {@link OnScaleGestureListener} but does nothing.
    106      * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
    107      * {@code false} so that a subclass can retrieve the accumulated scale
    108      * factor in an overridden onScaleEnd.
    109      * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
    110      * {@code true}.
    111      */
    112     public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
    113 
    114         public boolean onScale(ScaleGestureDetector detector) {
    115             return false;
    116         }
    117 
    118         public boolean onScaleBegin(ScaleGestureDetector detector) {
    119             return true;
    120         }
    121 
    122         public void onScaleEnd(ScaleGestureDetector detector) {
    123             // Intentionally empty
    124         }
    125     }
    126 
    127     private final Context mContext;
    128     private final OnScaleGestureListener mListener;
    129 
    130     private float mFocusX;
    131     private float mFocusY;
    132 
    133     private boolean mQuickScaleEnabled;
    134 
    135     private float mCurrSpan;
    136     private float mPrevSpan;
    137     private float mInitialSpan;
    138     private float mCurrSpanX;
    139     private float mCurrSpanY;
    140     private float mPrevSpanX;
    141     private float mPrevSpanY;
    142     private long mCurrTime;
    143     private long mPrevTime;
    144     private boolean mInProgress;
    145     private int mSpanSlop;
    146     private int mMinSpan;
    147 
    148     // Bounds for recently seen values
    149     private float mTouchUpper;
    150     private float mTouchLower;
    151     private float mTouchHistoryLastAccepted;
    152     private int mTouchHistoryDirection;
    153     private long mTouchHistoryLastAcceptedTime;
    154     private int mTouchMinMajor;
    155     private MotionEvent mDoubleTapEvent;
    156     private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
    157     private final Handler mHandler;
    158 
    159     private static final long TOUCH_STABILIZE_TIME = 128; // ms
    160     private static final int DOUBLE_TAP_MODE_NONE = 0;
    161     private static final int DOUBLE_TAP_MODE_IN_PROGRESS = 1;
    162     private static final float SCALE_FACTOR = .5f;
    163 
    164 
    165     /**
    166      * Consistency verifier for debugging purposes.
    167      */
    168     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
    169             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
    170                     new InputEventConsistencyVerifier(this, 0) : null;
    171     private GestureDetector mGestureDetector;
    172 
    173     private boolean mEventBeforeOrAboveStartingGestureEvent;
    174 
    175     /**
    176      * Creates a ScaleGestureDetector with the supplied listener.
    177      * You may only use this constructor from a {@link android.os.Looper Looper} thread.
    178      *
    179      * @param context the application's context
    180      * @param listener the listener invoked for all the callbacks, this must
    181      * not be null.
    182      *
    183      * @throws NullPointerException if {@code listener} is null.
    184      */
    185     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
    186         this(context, listener, null);
    187     }
    188 
    189     /**
    190      * Creates a ScaleGestureDetector with the supplied listener.
    191      * @see android.os.Handler#Handler()
    192      *
    193      * @param context the application's context
    194      * @param listener the listener invoked for all the callbacks, this must
    195      * not be null.
    196      * @param handler the handler to use for running deferred listener events.
    197      *
    198      * @throws NullPointerException if {@code listener} is null.
    199      */
    200     public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
    201                                 Handler handler) {
    202         mContext = context;
    203         mListener = listener;
    204         mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
    205 
    206         final Resources res = context.getResources();
    207         mTouchMinMajor = res.getDimensionPixelSize(
    208                 com.android.internal.R.dimen.config_minScalingTouchMajor);
    209         mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
    210         mHandler = handler;
    211         // Quick scale is enabled by default after JB_MR2
    212         if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
    213             setQuickScaleEnabled(true);
    214         }
    215     }
    216 
    217     /**
    218      * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
    219      * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
    220      * @param ev MotionEvent to add to the ongoing history
    221      */
    222     private void addTouchHistory(MotionEvent ev) {
    223         final long currentTime = SystemClock.uptimeMillis();
    224         final int count = ev.getPointerCount();
    225         boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
    226         float total = 0;
    227         int sampleCount = 0;
    228         for (int i = 0; i < count; i++) {
    229             final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
    230             final int historySize = ev.getHistorySize();
    231             final int pointerSampleCount = historySize + 1;
    232             for (int h = 0; h < pointerSampleCount; h++) {
    233                 float major;
    234                 if (h < historySize) {
    235                     major = ev.getHistoricalTouchMajor(i, h);
    236                 } else {
    237                     major = ev.getTouchMajor(i);
    238                 }
    239                 if (major < mTouchMinMajor) major = mTouchMinMajor;
    240                 total += major;
    241 
    242                 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
    243                     mTouchUpper = major;
    244                 }
    245                 if (Float.isNaN(mTouchLower) || major < mTouchLower) {
    246                     mTouchLower = major;
    247                 }
    248 
    249                 if (hasLastAccepted) {
    250                     final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
    251                     if (directionSig != mTouchHistoryDirection ||
    252                             (directionSig == 0 && mTouchHistoryDirection == 0)) {
    253                         mTouchHistoryDirection = directionSig;
    254                         final long time = h < historySize ? ev.getHistoricalEventTime(h)
    255                                 : ev.getEventTime();
    256                         mTouchHistoryLastAcceptedTime = time;
    257                         accept = false;
    258                     }
    259                 }
    260             }
    261             sampleCount += pointerSampleCount;
    262         }
    263 
    264         final float avg = total / sampleCount;
    265 
    266         if (accept) {
    267             float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
    268             mTouchUpper = (mTouchUpper + newAccepted) / 2;
    269             mTouchLower = (mTouchLower + newAccepted) / 2;
    270             mTouchHistoryLastAccepted = newAccepted;
    271             mTouchHistoryDirection = 0;
    272             mTouchHistoryLastAcceptedTime = ev.getEventTime();
    273         }
    274     }
    275 
    276     /**
    277      * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
    278      * @see #addTouchHistory(MotionEvent)
    279      */
    280     private void clearTouchHistory() {
    281         mTouchUpper = Float.NaN;
    282         mTouchLower = Float.NaN;
    283         mTouchHistoryLastAccepted = Float.NaN;
    284         mTouchHistoryDirection = 0;
    285         mTouchHistoryLastAcceptedTime = 0;
    286     }
    287 
    288     /**
    289      * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
    290      * when appropriate.
    291      *
    292      * <p>Applications should pass a complete and consistent event stream to this method.
    293      * A complete and consistent event stream involves all MotionEvents from the initial
    294      * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
    295      *
    296      * @param event The event to process
    297      * @return true if the event was processed and the detector wants to receive the
    298      *         rest of the MotionEvents in this event stream.
    299      */
    300     public boolean onTouchEvent(MotionEvent event) {
    301         if (mInputEventConsistencyVerifier != null) {
    302             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    303         }
    304 
    305         mCurrTime = event.getEventTime();
    306 
    307         final int action = event.getActionMasked();
    308 
    309         // Forward the event to check for double tap gesture
    310         if (mQuickScaleEnabled) {
    311             mGestureDetector.onTouchEvent(event);
    312         }
    313 
    314         final boolean streamComplete = action == MotionEvent.ACTION_UP ||
    315                 action == MotionEvent.ACTION_CANCEL;
    316 
    317         if (action == MotionEvent.ACTION_DOWN || streamComplete) {
    318             // Reset any scale in progress with the listener.
    319             // If it's an ACTION_DOWN we're beginning a new event stream.
    320             // This means the app probably didn't give us all the events. Shame on it.
    321             if (mInProgress) {
    322                 mListener.onScaleEnd(this);
    323                 mInProgress = false;
    324                 mInitialSpan = 0;
    325                 mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
    326             } else if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS && streamComplete) {
    327                 mInProgress = false;
    328                 mInitialSpan = 0;
    329                 mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
    330             }
    331 
    332             if (streamComplete) {
    333                 clearTouchHistory();
    334                 return true;
    335             }
    336         }
    337 
    338         final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
    339                 action == MotionEvent.ACTION_POINTER_UP ||
    340                 action == MotionEvent.ACTION_POINTER_DOWN;
    341 
    342 
    343         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
    344         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
    345 
    346         // Determine focal point
    347         float sumX = 0, sumY = 0;
    348         final int count = event.getPointerCount();
    349         final int div = pointerUp ? count - 1 : count;
    350         final float focusX;
    351         final float focusY;
    352         if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) {
    353             // In double tap mode, the focal pt is always where the double tap
    354             // gesture started
    355             focusX = mDoubleTapEvent.getX();
    356             focusY = mDoubleTapEvent.getY();
    357             if (event.getY() < focusY) {
    358                 mEventBeforeOrAboveStartingGestureEvent = true;
    359             } else {
    360                 mEventBeforeOrAboveStartingGestureEvent = false;
    361             }
    362         } else {
    363             for (int i = 0; i < count; i++) {
    364                 if (skipIndex == i) continue;
    365                 sumX += event.getX(i);
    366                 sumY += event.getY(i);
    367             }
    368 
    369             focusX = sumX / div;
    370             focusY = sumY / div;
    371         }
    372 
    373         addTouchHistory(event);
    374 
    375         // Determine average deviation from focal point
    376         float devSumX = 0, devSumY = 0;
    377         for (int i = 0; i < count; i++) {
    378             if (skipIndex == i) continue;
    379 
    380             // Convert the resulting diameter into a radius.
    381             final float touchSize = mTouchHistoryLastAccepted / 2;
    382             devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
    383             devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
    384         }
    385         final float devX = devSumX / div;
    386         final float devY = devSumY / div;
    387 
    388         // Span is the average distance between touch points through the focal point;
    389         // i.e. the diameter of the circle with a radius of the average deviation from
    390         // the focal point.
    391         final float spanX = devX * 2;
    392         final float spanY = devY * 2;
    393         final float span;
    394         if (inDoubleTapMode()) {
    395             span = spanY;
    396         } else {
    397             span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
    398         }
    399 
    400         // Dispatch begin/end events as needed.
    401         // If the configuration changes, notify the app to reset its current state by beginning
    402         // a fresh scale event stream.
    403         final boolean wasInProgress = mInProgress;
    404         mFocusX = focusX;
    405         mFocusY = focusY;
    406         if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) {
    407             mListener.onScaleEnd(this);
    408             mInProgress = false;
    409             mInitialSpan = span;
    410             mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
    411         }
    412         if (configChanged) {
    413             mPrevSpanX = mCurrSpanX = spanX;
    414             mPrevSpanY = mCurrSpanY = spanY;
    415             mInitialSpan = mPrevSpan = mCurrSpan = span;
    416         }
    417 
    418         final int minSpan = inDoubleTapMode() ? mSpanSlop : mMinSpan;
    419         if (!mInProgress && span >=  minSpan &&
    420                 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
    421             mPrevSpanX = mCurrSpanX = spanX;
    422             mPrevSpanY = mCurrSpanY = spanY;
    423             mPrevSpan = mCurrSpan = span;
    424             mPrevTime = mCurrTime;
    425             mInProgress = mListener.onScaleBegin(this);
    426         }
    427 
    428         // Handle motion; focal point and span/scale factor are changing.
    429         if (action == MotionEvent.ACTION_MOVE) {
    430             mCurrSpanX = spanX;
    431             mCurrSpanY = spanY;
    432             mCurrSpan = span;
    433 
    434             boolean updatePrev = true;
    435 
    436             if (mInProgress) {
    437                 updatePrev = mListener.onScale(this);
    438             }
    439 
    440             if (updatePrev) {
    441                 mPrevSpanX = mCurrSpanX;
    442                 mPrevSpanY = mCurrSpanY;
    443                 mPrevSpan = mCurrSpan;
    444                 mPrevTime = mCurrTime;
    445             }
    446         }
    447 
    448         return true;
    449     }
    450 
    451 
    452     private boolean inDoubleTapMode() {
    453         return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS;
    454     }
    455 
    456     /**
    457      * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
    458      * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
    459      * if the app targets API 19 and newer.
    460      * @param scales true to enable quick scaling, false to disable
    461      */
    462     public void setQuickScaleEnabled(boolean scales) {
    463         mQuickScaleEnabled = scales;
    464         if (mQuickScaleEnabled && mGestureDetector == null) {
    465             GestureDetector.SimpleOnGestureListener gestureListener =
    466                     new GestureDetector.SimpleOnGestureListener() {
    467                         @Override
    468                         public boolean onDoubleTap(MotionEvent e) {
    469                             // Double tap: start watching for a swipe
    470                             mDoubleTapEvent = e;
    471                             mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS;
    472                             return true;
    473                         }
    474                     };
    475             mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
    476         }
    477     }
    478 
    479   /**
    480    * Return whether the quick scale gesture, in which the user performs a double tap followed by a
    481    * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
    482    */
    483     public boolean isQuickScaleEnabled() {
    484         return mQuickScaleEnabled;
    485     }
    486 
    487     /**
    488      * Returns {@code true} if a scale gesture is in progress.
    489      */
    490     public boolean isInProgress() {
    491         return mInProgress;
    492     }
    493 
    494     /**
    495      * Get the X coordinate of the current gesture's focal point.
    496      * If a gesture is in progress, the focal point is between
    497      * each of the pointers forming the gesture.
    498      *
    499      * If {@link #isInProgress()} would return false, the result of this
    500      * function is undefined.
    501      *
    502      * @return X coordinate of the focal point in pixels.
    503      */
    504     public float getFocusX() {
    505         return mFocusX;
    506     }
    507 
    508     /**
    509      * Get the Y coordinate of the current gesture's focal point.
    510      * If a gesture is in progress, the focal point is between
    511      * each of the pointers forming the gesture.
    512      *
    513      * If {@link #isInProgress()} would return false, the result of this
    514      * function is undefined.
    515      *
    516      * @return Y coordinate of the focal point in pixels.
    517      */
    518     public float getFocusY() {
    519         return mFocusY;
    520     }
    521 
    522     /**
    523      * Return the average distance between each of the pointers forming the
    524      * gesture in progress through the focal point.
    525      *
    526      * @return Distance between pointers in pixels.
    527      */
    528     public float getCurrentSpan() {
    529         return mCurrSpan;
    530     }
    531 
    532     /**
    533      * Return the average X distance between each of the pointers forming the
    534      * gesture in progress through the focal point.
    535      *
    536      * @return Distance between pointers in pixels.
    537      */
    538     public float getCurrentSpanX() {
    539         return mCurrSpanX;
    540     }
    541 
    542     /**
    543      * Return the average Y distance between each of the pointers forming the
    544      * gesture in progress through the focal point.
    545      *
    546      * @return Distance between pointers in pixels.
    547      */
    548     public float getCurrentSpanY() {
    549         return mCurrSpanY;
    550     }
    551 
    552     /**
    553      * Return the previous average distance between each of the pointers forming the
    554      * gesture in progress through the focal point.
    555      *
    556      * @return Previous distance between pointers in pixels.
    557      */
    558     public float getPreviousSpan() {
    559         return mPrevSpan;
    560     }
    561 
    562     /**
    563      * Return the previous average X distance between each of the pointers forming the
    564      * gesture in progress through the focal point.
    565      *
    566      * @return Previous distance between pointers in pixels.
    567      */
    568     public float getPreviousSpanX() {
    569         return mPrevSpanX;
    570     }
    571 
    572     /**
    573      * Return the previous average Y distance between each of the pointers forming the
    574      * gesture in progress through the focal point.
    575      *
    576      * @return Previous distance between pointers in pixels.
    577      */
    578     public float getPreviousSpanY() {
    579         return mPrevSpanY;
    580     }
    581 
    582     /**
    583      * Return the scaling factor from the previous scale event to the current
    584      * event. This value is defined as
    585      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
    586      *
    587      * @return The current scaling factor.
    588      */
    589     public float getScaleFactor() {
    590         if (inDoubleTapMode()) {
    591             // Drag is moving up; the further away from the gesture
    592             // start, the smaller the span should be, the closer,
    593             // the larger the span, and therefore the larger the scale
    594             final boolean scaleUp =
    595                     (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
    596                     (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
    597             final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
    598             return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
    599         }
    600         return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    601     }
    602 
    603     /**
    604      * Return the time difference in milliseconds between the previous
    605      * accepted scaling event and the current scaling event.
    606      *
    607      * @return Time difference since the last scaling event in milliseconds.
    608      */
    609     public long getTimeDelta() {
    610         return mCurrTime - mPrevTime;
    611     }
    612 
    613     /**
    614      * Return the event time of the current event being processed.
    615      *
    616      * @return Current event time in milliseconds.
    617      */
    618     public long getEventTime() {
    619         return mCurrTime;
    620     }
    621 }