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