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