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.SystemClock;
     22 import android.util.FloatMath;
     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 float mCurrSpan;
    132     private float mPrevSpan;
    133     private float mInitialSpan;
    134     private float mCurrSpanX;
    135     private float mCurrSpanY;
    136     private float mPrevSpanX;
    137     private float mPrevSpanY;
    138     private long mCurrTime;
    139     private long mPrevTime;
    140     private boolean mInProgress;
    141     private int mSpanSlop;
    142     private int mMinSpan;
    143 
    144     // Bounds for recently seen values
    145     private float mTouchUpper;
    146     private float mTouchLower;
    147     private float mTouchHistoryLastAccepted;
    148     private int mTouchHistoryDirection;
    149     private long mTouchHistoryLastAcceptedTime;
    150     private int mTouchMinMajor;
    151 
    152     private static final long TOUCH_STABILIZE_TIME = 128; // ms
    153     private static final int TOUCH_MIN_MAJOR = 48; // dp
    154 
    155     /**
    156      * Consistency verifier for debugging purposes.
    157      */
    158     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
    159             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
    160                     new InputEventConsistencyVerifier(this, 0) : null;
    161 
    162     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
    163         mContext = context;
    164         mListener = listener;
    165         mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
    166 
    167         final Resources res = context.getResources();
    168         mTouchMinMajor = res.getDimensionPixelSize(
    169                 com.android.internal.R.dimen.config_minScalingTouchMajor);
    170         mMinSpan = res.getDimensionPixelSize(
    171                 com.android.internal.R.dimen.config_minScalingSpan);
    172     }
    173 
    174     /**
    175      * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
    176      * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
    177      * @param ev MotionEvent to add to the ongoing history
    178      */
    179     private void addTouchHistory(MotionEvent ev) {
    180         final long currentTime = SystemClock.uptimeMillis();
    181         final int count = ev.getPointerCount();
    182         boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
    183         float total = 0;
    184         int sampleCount = 0;
    185         for (int i = 0; i < count; i++) {
    186             final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
    187             final int historySize = ev.getHistorySize();
    188             final int pointerSampleCount = historySize + 1;
    189             for (int h = 0; h < pointerSampleCount; h++) {
    190                 float major;
    191                 if (h < historySize) {
    192                     major = ev.getHistoricalTouchMajor(i, h);
    193                 } else {
    194                     major = ev.getTouchMajor(i);
    195                 }
    196                 if (major < mTouchMinMajor) major = mTouchMinMajor;
    197                 total += major;
    198 
    199                 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
    200                     mTouchUpper = major;
    201                 }
    202                 if (Float.isNaN(mTouchLower) || major < mTouchLower) {
    203                     mTouchLower = major;
    204                 }
    205 
    206                 if (hasLastAccepted) {
    207                     final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
    208                     if (directionSig != mTouchHistoryDirection ||
    209                             (directionSig == 0 && mTouchHistoryDirection == 0)) {
    210                         mTouchHistoryDirection = directionSig;
    211                         final long time = h < historySize ? ev.getHistoricalEventTime(h)
    212                                 : ev.getEventTime();
    213                         mTouchHistoryLastAcceptedTime = time;
    214                         accept = false;
    215                     }
    216                 }
    217             }
    218             sampleCount += pointerSampleCount;
    219         }
    220 
    221         final float avg = total / sampleCount;
    222 
    223         if (accept) {
    224             float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
    225             mTouchUpper = (mTouchUpper + newAccepted) / 2;
    226             mTouchLower = (mTouchLower + newAccepted) / 2;
    227             mTouchHistoryLastAccepted = newAccepted;
    228             mTouchHistoryDirection = 0;
    229             mTouchHistoryLastAcceptedTime = ev.getEventTime();
    230         }
    231     }
    232 
    233     /**
    234      * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
    235      * @see #addTouchHistory(MotionEvent)
    236      */
    237     private void clearTouchHistory() {
    238         mTouchUpper = Float.NaN;
    239         mTouchLower = Float.NaN;
    240         mTouchHistoryLastAccepted = Float.NaN;
    241         mTouchHistoryDirection = 0;
    242         mTouchHistoryLastAcceptedTime = 0;
    243     }
    244 
    245     /**
    246      * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
    247      * when appropriate.
    248      *
    249      * <p>Applications should pass a complete and consistent event stream to this method.
    250      * A complete and consistent event stream involves all MotionEvents from the initial
    251      * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
    252      *
    253      * @param event The event to process
    254      * @return true if the event was processed and the detector wants to receive the
    255      *         rest of the MotionEvents in this event stream.
    256      */
    257     public boolean onTouchEvent(MotionEvent event) {
    258         if (mInputEventConsistencyVerifier != null) {
    259             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    260         }
    261 
    262         mCurrTime = event.getEventTime();
    263 
    264         final int action = event.getActionMasked();
    265 
    266         final boolean streamComplete = action == MotionEvent.ACTION_UP ||
    267                 action == MotionEvent.ACTION_CANCEL;
    268         if (action == MotionEvent.ACTION_DOWN || streamComplete) {
    269             // Reset any scale in progress with the listener.
    270             // If it's an ACTION_DOWN we're beginning a new event stream.
    271             // This means the app probably didn't give us all the events. Shame on it.
    272             if (mInProgress) {
    273                 mListener.onScaleEnd(this);
    274                 mInProgress = false;
    275                 mInitialSpan = 0;
    276             }
    277 
    278             if (streamComplete) {
    279                 clearTouchHistory();
    280                 return true;
    281             }
    282         }
    283 
    284         final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
    285                 action == MotionEvent.ACTION_POINTER_UP ||
    286                 action == MotionEvent.ACTION_POINTER_DOWN;
    287         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
    288         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
    289 
    290         // Determine focal point
    291         float sumX = 0, sumY = 0;
    292         final int count = event.getPointerCount();
    293         for (int i = 0; i < count; i++) {
    294             if (skipIndex == i) continue;
    295             sumX += event.getX(i);
    296             sumY += event.getY(i);
    297         }
    298         final int div = pointerUp ? count - 1 : count;
    299         final float focusX = sumX / div;
    300         final float focusY = sumY / div;
    301 
    302 
    303         addTouchHistory(event);
    304 
    305         // Determine average deviation from focal point
    306         float devSumX = 0, devSumY = 0;
    307         for (int i = 0; i < count; i++) {
    308             if (skipIndex == i) continue;
    309 
    310             // Convert the resulting diameter into a radius.
    311             final float touchSize = mTouchHistoryLastAccepted / 2;
    312             devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
    313             devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
    314         }
    315         final float devX = devSumX / div;
    316         final float devY = devSumY / div;
    317 
    318         // Span is the average distance between touch points through the focal point;
    319         // i.e. the diameter of the circle with a radius of the average deviation from
    320         // the focal point.
    321         final float spanX = devX * 2;
    322         final float spanY = devY * 2;
    323         final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
    324 
    325         // Dispatch begin/end events as needed.
    326         // If the configuration changes, notify the app to reset its current state by beginning
    327         // a fresh scale event stream.
    328         final boolean wasInProgress = mInProgress;
    329         mFocusX = focusX;
    330         mFocusY = focusY;
    331         if (mInProgress && (span < mMinSpan || configChanged)) {
    332             mListener.onScaleEnd(this);
    333             mInProgress = false;
    334             mInitialSpan = span;
    335         }
    336         if (configChanged) {
    337             mPrevSpanX = mCurrSpanX = spanX;
    338             mPrevSpanY = mCurrSpanY = spanY;
    339             mInitialSpan = mPrevSpan = mCurrSpan = span;
    340         }
    341         if (!mInProgress && span >= mMinSpan &&
    342                 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
    343             mPrevSpanX = mCurrSpanX = spanX;
    344             mPrevSpanY = mCurrSpanY = spanY;
    345             mPrevSpan = mCurrSpan = span;
    346             mPrevTime = mCurrTime;
    347             mInProgress = mListener.onScaleBegin(this);
    348         }
    349 
    350         // Handle motion; focal point and span/scale factor are changing.
    351         if (action == MotionEvent.ACTION_MOVE) {
    352             mCurrSpanX = spanX;
    353             mCurrSpanY = spanY;
    354             mCurrSpan = span;
    355 
    356             boolean updatePrev = true;
    357             if (mInProgress) {
    358                 updatePrev = mListener.onScale(this);
    359             }
    360 
    361             if (updatePrev) {
    362                 mPrevSpanX = mCurrSpanX;
    363                 mPrevSpanY = mCurrSpanY;
    364                 mPrevSpan = mCurrSpan;
    365                 mPrevTime = mCurrTime;
    366             }
    367         }
    368 
    369         return true;
    370     }
    371 
    372     /**
    373      * Returns {@code true} if a scale gesture is in progress.
    374      */
    375     public boolean isInProgress() {
    376         return mInProgress;
    377     }
    378 
    379     /**
    380      * Get the X coordinate of the current gesture's focal point.
    381      * If a gesture is in progress, the focal point is between
    382      * each of the pointers forming the gesture.
    383      *
    384      * If {@link #isInProgress()} would return false, the result of this
    385      * function is undefined.
    386      *
    387      * @return X coordinate of the focal point in pixels.
    388      */
    389     public float getFocusX() {
    390         return mFocusX;
    391     }
    392 
    393     /**
    394      * Get the Y coordinate of the current gesture's focal point.
    395      * If a gesture is in progress, the focal point is between
    396      * each of the pointers forming the gesture.
    397      *
    398      * If {@link #isInProgress()} would return false, the result of this
    399      * function is undefined.
    400      *
    401      * @return Y coordinate of the focal point in pixels.
    402      */
    403     public float getFocusY() {
    404         return mFocusY;
    405     }
    406 
    407     /**
    408      * Return the average distance between each of the pointers forming the
    409      * gesture in progress through the focal point.
    410      *
    411      * @return Distance between pointers in pixels.
    412      */
    413     public float getCurrentSpan() {
    414         return mCurrSpan;
    415     }
    416 
    417     /**
    418      * Return the average X distance between each of the pointers forming the
    419      * gesture in progress through the focal point.
    420      *
    421      * @return Distance between pointers in pixels.
    422      */
    423     public float getCurrentSpanX() {
    424         return mCurrSpanX;
    425     }
    426 
    427     /**
    428      * Return the average Y distance between each of the pointers forming the
    429      * gesture in progress through the focal point.
    430      *
    431      * @return Distance between pointers in pixels.
    432      */
    433     public float getCurrentSpanY() {
    434         return mCurrSpanY;
    435     }
    436 
    437     /**
    438      * Return the previous average distance between each of the pointers forming the
    439      * gesture in progress through the focal point.
    440      *
    441      * @return Previous distance between pointers in pixels.
    442      */
    443     public float getPreviousSpan() {
    444         return mPrevSpan;
    445     }
    446 
    447     /**
    448      * Return the previous average X distance between each of the pointers forming the
    449      * gesture in progress through the focal point.
    450      *
    451      * @return Previous distance between pointers in pixels.
    452      */
    453     public float getPreviousSpanX() {
    454         return mPrevSpanX;
    455     }
    456 
    457     /**
    458      * Return the previous average Y distance between each of the pointers forming the
    459      * gesture in progress through the focal point.
    460      *
    461      * @return Previous distance between pointers in pixels.
    462      */
    463     public float getPreviousSpanY() {
    464         return mPrevSpanY;
    465     }
    466 
    467     /**
    468      * Return the scaling factor from the previous scale event to the current
    469      * event. This value is defined as
    470      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
    471      *
    472      * @return The current scaling factor.
    473      */
    474     public float getScaleFactor() {
    475         return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    476     }
    477 
    478     /**
    479      * Return the time difference in milliseconds between the previous
    480      * accepted scaling event and the current scaling event.
    481      *
    482      * @return Time difference since the last scaling event in milliseconds.
    483      */
    484     public long getTimeDelta() {
    485         return mCurrTime - mPrevTime;
    486     }
    487 
    488     /**
    489      * Return the event time of the current event being processed.
    490      *
    491      * @return Current event time in milliseconds.
    492      */
    493     public long getEventTime() {
    494         return mCurrTime;
    495     }
    496 }
    497