• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onScale(ScaleGestureDetector detector)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          */
onScaleBegin(ScaleGestureDetector detector)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          */
onScaleEnd(ScaleGestureDetector detector)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 
onScale(ScaleGestureDetector detector)113         public boolean onScale(ScaleGestureDetector detector) {
114             return false;
115         }
116 
onScaleBegin(ScaleGestureDetector detector)117         public boolean onScaleBegin(ScaleGestureDetector detector) {
118             return true;
119         }
120 
onScaleEnd(ScaleGestureDetector detector)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      */
ScaleGestureDetector(Context context, OnScaleGestureListener listener)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      */
ScaleGestureDetector(Context context, OnScaleGestureListener listener, Handler handler)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      */
onTouchEvent(MotionEvent event)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 
inAnchoredScaleMode()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      */
setQuickScaleEnabled(boolean scales)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    */
isQuickScaleEnabled()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      */
setStylusScaleEnabled(boolean scales)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      */
isStylusScaleEnabled()438     public boolean isStylusScaleEnabled() {
439         return mStylusScaleEnabled;
440     }
441 
442     /**
443      * Returns {@code true} if a scale gesture is in progress.
444      */
isInProgress()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      */
getFocusX()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      */
getFocusY()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      */
getCurrentSpan()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      */
getCurrentSpanX()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      */
getCurrentSpanY()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      */
getPreviousSpan()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      */
getPreviousSpanX()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      */
getPreviousSpanY()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      */
getScaleFactor()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      */
getTimeDelta()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      */
getEventTime()573     public long getEventTime() {
574         return mCurrTime;
575     }
576 }