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