• 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.compat.annotation.UnsupportedAppUsage;
20 import android.content.Context;
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          */
onScale(ScaleGestureDetector detector)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          */
onScaleBegin(ScaleGestureDetector detector)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          */
onScaleEnd(ScaleGestureDetector detector)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 
onScale(ScaleGestureDetector detector)112         public boolean onScale(ScaleGestureDetector detector) {
113             return false;
114         }
115 
onScaleBegin(ScaleGestureDetector detector)116         public boolean onScaleBegin(ScaleGestureDetector detector) {
117             return true;
118         }
119 
onScaleEnd(ScaleGestureDetector detector)120         public void onScaleEnd(ScaleGestureDetector detector) {
121             // Intentionally empty
122         }
123     }
124 
125     private final Context mContext;
126     @UnsupportedAppUsage
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     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938)
146     private int mSpanSlop;
147     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938)
148     private int mMinSpan;
149 
150     private final Handler mHandler;
151 
152     private float mAnchoredScaleStartX;
153     private float mAnchoredScaleStartY;
154     private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
155 
156     private static final long TOUCH_STABILIZE_TIME = 128; // ms
157     private static final float SCALE_FACTOR = .5f;
158     private static final int ANCHORED_SCALE_MODE_NONE = 0;
159     private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
160     private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
161 
162 
163     /**
164      * Consistency verifier for debugging purposes.
165      */
166     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
167             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
168                     new InputEventConsistencyVerifier(this, 0) : null;
169     private GestureDetector mGestureDetector;
170 
171     private boolean mEventBeforeOrAboveStartingGestureEvent;
172 
173     /**
174      * Creates a ScaleGestureDetector with the supplied listener.
175      * You may only use this constructor from a {@link android.os.Looper Looper} thread.
176      *
177      * @param context the application's context
178      * @param listener the listener invoked for all the callbacks, this must
179      * not be null.
180      *
181      * @throws NullPointerException if {@code listener} is null.
182      */
ScaleGestureDetector(Context context, OnScaleGestureListener listener)183     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
184         this(context, listener, null);
185     }
186 
187     /**
188      * Creates a ScaleGestureDetector with the supplied listener.
189      * @see android.os.Handler#Handler()
190      *
191      * @param context the application's context
192      * @param listener the listener invoked for all the callbacks, this must
193      * not be null.
194      * @param handler the handler to use for running deferred listener events.
195      *
196      * @throws NullPointerException if {@code listener} is null.
197      */
ScaleGestureDetector(Context context, OnScaleGestureListener listener, Handler handler)198     public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
199                                 Handler handler) {
200         mContext = context;
201         mListener = listener;
202         final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
203         mSpanSlop = viewConfiguration.getScaledTouchSlop() * 2;
204         mMinSpan = viewConfiguration.getScaledMinimumScalingSpan();
205         mHandler = handler;
206         // Quick scale is enabled by default after JB_MR2
207         final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
208         if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
209             setQuickScaleEnabled(true);
210         }
211         // Stylus scale is enabled by default after LOLLIPOP_MR1
212         if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
213             setStylusScaleEnabled(true);
214         }
215     }
216 
217     /**
218      * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
219      * when appropriate.
220      *
221      * <p>Applications should pass a complete and consistent event stream to this method.
222      * A complete and consistent event stream involves all MotionEvents from the initial
223      * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
224      *
225      * @param event The event to process
226      * @return true if the event was processed and the detector wants to receive the
227      *         rest of the MotionEvents in this event stream.
228      */
onTouchEvent(MotionEvent event)229     public boolean onTouchEvent(MotionEvent event) {
230         if (mInputEventConsistencyVerifier != null) {
231             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
232         }
233 
234         mCurrTime = event.getEventTime();
235 
236         final int action = event.getActionMasked();
237 
238         // Forward the event to check for double tap gesture
239         if (mQuickScaleEnabled) {
240             mGestureDetector.onTouchEvent(event);
241         }
242 
243         final int count = event.getPointerCount();
244         final boolean isStylusButtonDown =
245                 (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
246 
247         final boolean anchoredScaleCancelled =
248                 mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
249         final boolean streamComplete = action == MotionEvent.ACTION_UP ||
250                 action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
251 
252         if (action == MotionEvent.ACTION_DOWN || streamComplete) {
253             // Reset any scale in progress with the listener.
254             // If it's an ACTION_DOWN we're beginning a new event stream.
255             // This means the app probably didn't give us all the events. Shame on it.
256             if (mInProgress) {
257                 mListener.onScaleEnd(this);
258                 mInProgress = false;
259                 mInitialSpan = 0;
260                 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
261             } else if (inAnchoredScaleMode() && streamComplete) {
262                 mInProgress = false;
263                 mInitialSpan = 0;
264                 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
265             }
266 
267             if (streamComplete) {
268                 return true;
269             }
270         }
271 
272         if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
273                 && !streamComplete && isStylusButtonDown) {
274             // Start of a button scale gesture
275             mAnchoredScaleStartX = event.getX();
276             mAnchoredScaleStartY = event.getY();
277             mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
278             mInitialSpan = 0;
279         }
280 
281         final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
282                 action == MotionEvent.ACTION_POINTER_UP ||
283                 action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
284 
285         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
286         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
287 
288         // Determine focal point
289         float sumX = 0, sumY = 0;
290         final int div = pointerUp ? count - 1 : count;
291         final float focusX;
292         final float focusY;
293         if (inAnchoredScaleMode()) {
294             // In anchored scale mode, the focal pt is always where the double tap
295             // or button down gesture started
296             focusX = mAnchoredScaleStartX;
297             focusY = mAnchoredScaleStartY;
298             if (event.getY() < focusY) {
299                 mEventBeforeOrAboveStartingGestureEvent = true;
300             } else {
301                 mEventBeforeOrAboveStartingGestureEvent = false;
302             }
303         } else {
304             for (int i = 0; i < count; i++) {
305                 if (skipIndex == i) continue;
306                 sumX += event.getX(i);
307                 sumY += event.getY(i);
308             }
309 
310             focusX = sumX / div;
311             focusY = sumY / div;
312         }
313 
314         // Determine average deviation from focal point
315         float devSumX = 0, devSumY = 0;
316         for (int i = 0; i < count; i++) {
317             if (skipIndex == i) continue;
318 
319             // Convert the resulting diameter into a radius.
320             devSumX += Math.abs(event.getX(i) - focusX);
321             devSumY += Math.abs(event.getY(i) - focusY);
322         }
323         final float devX = devSumX / div;
324         final float devY = devSumY / div;
325 
326         // Span is the average distance between touch points through the focal point;
327         // i.e. the diameter of the circle with a radius of the average deviation from
328         // the focal point.
329         final float spanX = devX * 2;
330         final float spanY = devY * 2;
331         final float span;
332         if (inAnchoredScaleMode()) {
333             span = spanY;
334         } else {
335             span = (float) Math.hypot(spanX, spanY);
336         }
337 
338         // Dispatch begin/end events as needed.
339         // If the configuration changes, notify the app to reset its current state by beginning
340         // a fresh scale event stream.
341         final boolean wasInProgress = mInProgress;
342         mFocusX = focusX;
343         mFocusY = focusY;
344         if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
345             mListener.onScaleEnd(this);
346             mInProgress = false;
347             mInitialSpan = span;
348         }
349         if (configChanged) {
350             mPrevSpanX = mCurrSpanX = spanX;
351             mPrevSpanY = mCurrSpanY = spanY;
352             mInitialSpan = mPrevSpan = mCurrSpan = span;
353         }
354 
355         final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
356         if (!mInProgress && span >=  minSpan &&
357                 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
358             mPrevSpanX = mCurrSpanX = spanX;
359             mPrevSpanY = mCurrSpanY = spanY;
360             mPrevSpan = mCurrSpan = span;
361             mPrevTime = mCurrTime;
362             mInProgress = mListener.onScaleBegin(this);
363         }
364 
365         // Handle motion; focal point and span/scale factor are changing.
366         if (action == MotionEvent.ACTION_MOVE) {
367             mCurrSpanX = spanX;
368             mCurrSpanY = spanY;
369             mCurrSpan = span;
370 
371             boolean updatePrev = true;
372 
373             if (mInProgress) {
374                 updatePrev = mListener.onScale(this);
375             }
376 
377             if (updatePrev) {
378                 mPrevSpanX = mCurrSpanX;
379                 mPrevSpanY = mCurrSpanY;
380                 mPrevSpan = mCurrSpan;
381                 mPrevTime = mCurrTime;
382             }
383         }
384 
385         return true;
386     }
387 
inAnchoredScaleMode()388     private boolean inAnchoredScaleMode() {
389         return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
390     }
391 
392     /**
393      * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
394      * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
395      * if the app targets API 19 and newer.
396      * @param scales true to enable quick scaling, false to disable
397      */
setQuickScaleEnabled(boolean scales)398     public void setQuickScaleEnabled(boolean scales) {
399         mQuickScaleEnabled = scales;
400         if (mQuickScaleEnabled && mGestureDetector == null) {
401             GestureDetector.SimpleOnGestureListener gestureListener =
402                     new GestureDetector.SimpleOnGestureListener() {
403                         @Override
404                         public boolean onDoubleTap(MotionEvent e) {
405                             // Double tap: start watching for a swipe
406                             mAnchoredScaleStartX = e.getX();
407                             mAnchoredScaleStartY = e.getY();
408                             mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
409                             return true;
410                         }
411                     };
412             mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
413         }
414     }
415 
416   /**
417    * Return whether the quick scale gesture, in which the user performs a double tap followed by a
418    * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
419    */
isQuickScaleEnabled()420     public boolean isQuickScaleEnabled() {
421         return mQuickScaleEnabled;
422     }
423 
424     /**
425      * Sets whether the associates {@link OnScaleGestureListener} should receive
426      * onScale callbacks when the user uses a stylus and presses the button.
427      * Note that this is enabled by default if the app targets API 23 and newer.
428      *
429      * @param scales true to enable stylus scaling, false to disable.
430      */
setStylusScaleEnabled(boolean scales)431     public void setStylusScaleEnabled(boolean scales) {
432         mStylusScaleEnabled = scales;
433     }
434 
435     /**
436      * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
437      * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
438      */
isStylusScaleEnabled()439     public boolean isStylusScaleEnabled() {
440         return mStylusScaleEnabled;
441     }
442 
443     /**
444      * Returns {@code true} if a scale gesture is in progress.
445      */
isInProgress()446     public boolean isInProgress() {
447         return mInProgress;
448     }
449 
450     /**
451      * Get the X coordinate of the current gesture's focal point.
452      * If a gesture is in progress, the focal point is between
453      * each of the pointers forming the gesture.
454      *
455      * If {@link #isInProgress()} would return false, the result of this
456      * function is undefined.
457      *
458      * @return X coordinate of the focal point in pixels.
459      */
getFocusX()460     public float getFocusX() {
461         return mFocusX;
462     }
463 
464     /**
465      * Get the Y coordinate of the current gesture's focal point.
466      * If a gesture is in progress, the focal point is between
467      * each of the pointers forming the gesture.
468      *
469      * If {@link #isInProgress()} would return false, the result of this
470      * function is undefined.
471      *
472      * @return Y coordinate of the focal point in pixels.
473      */
getFocusY()474     public float getFocusY() {
475         return mFocusY;
476     }
477 
478     /**
479      * Return the average distance between each of the pointers forming the
480      * gesture in progress through the focal point.
481      *
482      * @return Distance between pointers in pixels.
483      */
getCurrentSpan()484     public float getCurrentSpan() {
485         return mCurrSpan;
486     }
487 
488     /**
489      * Return the average X distance between each of the pointers forming the
490      * gesture in progress through the focal point.
491      *
492      * @return Distance between pointers in pixels.
493      */
getCurrentSpanX()494     public float getCurrentSpanX() {
495         return mCurrSpanX;
496     }
497 
498     /**
499      * Return the average Y distance between each of the pointers forming the
500      * gesture in progress through the focal point.
501      *
502      * @return Distance between pointers in pixels.
503      */
getCurrentSpanY()504     public float getCurrentSpanY() {
505         return mCurrSpanY;
506     }
507 
508     /**
509      * Return the previous average distance between each of the pointers forming the
510      * gesture in progress through the focal point.
511      *
512      * @return Previous distance between pointers in pixels.
513      */
getPreviousSpan()514     public float getPreviousSpan() {
515         return mPrevSpan;
516     }
517 
518     /**
519      * Return the previous average X distance between each of the pointers forming the
520      * gesture in progress through the focal point.
521      *
522      * @return Previous distance between pointers in pixels.
523      */
getPreviousSpanX()524     public float getPreviousSpanX() {
525         return mPrevSpanX;
526     }
527 
528     /**
529      * Return the previous average Y distance between each of the pointers forming the
530      * gesture in progress through the focal point.
531      *
532      * @return Previous distance between pointers in pixels.
533      */
getPreviousSpanY()534     public float getPreviousSpanY() {
535         return mPrevSpanY;
536     }
537 
538     /**
539      * Return the scaling factor from the previous scale event to the current
540      * event. This value is defined as
541      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
542      *
543      * @return The current scaling factor.
544      */
getScaleFactor()545     public float getScaleFactor() {
546         if (inAnchoredScaleMode()) {
547             // Drag is moving up; the further away from the gesture
548             // start, the smaller the span should be, the closer,
549             // the larger the span, and therefore the larger the scale
550             final boolean scaleUp =
551                     (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
552                     (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
553             final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
554             return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
555         }
556         return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
557     }
558 
559     /**
560      * Return the time difference in milliseconds between the previous
561      * accepted scaling event and the current scaling event.
562      *
563      * @return Time difference since the last scaling event in milliseconds.
564      */
getTimeDelta()565     public long getTimeDelta() {
566         return mCurrTime - mPrevTime;
567     }
568 
569     /**
570      * Return the event time of the current event being processed.
571      *
572      * @return Current event time in milliseconds.
573      */
getEventTime()574     public long getEventTime() {
575         return mCurrTime;
576     }
577 }