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