• Home
  • History
  • Annotate
  • 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 import android.util.FloatMath;
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(ScaleGestureDetector detector)72         public boolean onScale(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(ScaleGestureDetector detector)86         public boolean onScaleBegin(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(ScaleGestureDetector detector)99         public void onScaleEnd(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(ScaleGestureDetector detector)114         public boolean onScale(ScaleGestureDetector detector) {
115             return false;
116         }
117 
onScaleBegin(ScaleGestureDetector detector)118         public boolean onScaleBegin(ScaleGestureDetector detector) {
119             return true;
120         }
121 
onScaleEnd(ScaleGestureDetector detector)122         public void onScaleEnd(ScaleGestureDetector detector) {
123             // Intentionally empty
124         }
125     }
126 
127     private final Context mContext;
128     private final OnScaleGestureListener mListener;
129 
130     private float mFocusX;
131     private float mFocusY;
132 
133     private boolean mQuickScaleEnabled;
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     // Bounds for recently seen values
149     private float mTouchUpper;
150     private float mTouchLower;
151     private float mTouchHistoryLastAccepted;
152     private int mTouchHistoryDirection;
153     private long mTouchHistoryLastAcceptedTime;
154     private int mTouchMinMajor;
155     private MotionEvent mDoubleTapEvent;
156     private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
157     private final Handler mHandler;
158 
159     private static final long TOUCH_STABILIZE_TIME = 128; // ms
160     private static final int DOUBLE_TAP_MODE_NONE = 0;
161     private static final int DOUBLE_TAP_MODE_IN_PROGRESS = 1;
162     private static final float SCALE_FACTOR = .5f;
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(Context context, OnScaleGestureListener listener)185     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
186         this(context, listener, null);
187     }
188 
189     /**
190      * Creates a ScaleGestureDetector with the supplied listener.
191      * @see android.os.Handler#Handler()
192      *
193      * @param context the application's context
194      * @param listener the listener invoked for all the callbacks, this must
195      * not be null.
196      * @param handler the handler to use for running deferred listener events.
197      *
198      * @throws NullPointerException if {@code listener} is null.
199      */
ScaleGestureDetector(Context context, OnScaleGestureListener listener, Handler handler)200     public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
201                                 Handler handler) {
202         mContext = context;
203         mListener = listener;
204         mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
205 
206         final Resources res = context.getResources();
207         mTouchMinMajor = res.getDimensionPixelSize(
208                 com.android.internal.R.dimen.config_minScalingTouchMajor);
209         mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
210         mHandler = handler;
211         // Quick scale is enabled by default after JB_MR2
212         if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
213             setQuickScaleEnabled(true);
214         }
215     }
216 
217     /**
218      * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
219      * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
220      * @param ev MotionEvent to add to the ongoing history
221      */
addTouchHistory(MotionEvent ev)222     private void addTouchHistory(MotionEvent ev) {
223         final long currentTime = SystemClock.uptimeMillis();
224         final int count = ev.getPointerCount();
225         boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
226         float total = 0;
227         int sampleCount = 0;
228         for (int i = 0; i < count; i++) {
229             final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
230             final int historySize = ev.getHistorySize();
231             final int pointerSampleCount = historySize + 1;
232             for (int h = 0; h < pointerSampleCount; h++) {
233                 float major;
234                 if (h < historySize) {
235                     major = ev.getHistoricalTouchMajor(i, h);
236                 } else {
237                     major = ev.getTouchMajor(i);
238                 }
239                 if (major < mTouchMinMajor) major = mTouchMinMajor;
240                 total += major;
241 
242                 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
243                     mTouchUpper = major;
244                 }
245                 if (Float.isNaN(mTouchLower) || major < mTouchLower) {
246                     mTouchLower = major;
247                 }
248 
249                 if (hasLastAccepted) {
250                     final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
251                     if (directionSig != mTouchHistoryDirection ||
252                             (directionSig == 0 && mTouchHistoryDirection == 0)) {
253                         mTouchHistoryDirection = directionSig;
254                         final long time = h < historySize ? ev.getHistoricalEventTime(h)
255                                 : ev.getEventTime();
256                         mTouchHistoryLastAcceptedTime = time;
257                         accept = false;
258                     }
259                 }
260             }
261             sampleCount += pointerSampleCount;
262         }
263 
264         final float avg = total / sampleCount;
265 
266         if (accept) {
267             float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
268             mTouchUpper = (mTouchUpper + newAccepted) / 2;
269             mTouchLower = (mTouchLower + newAccepted) / 2;
270             mTouchHistoryLastAccepted = newAccepted;
271             mTouchHistoryDirection = 0;
272             mTouchHistoryLastAcceptedTime = ev.getEventTime();
273         }
274     }
275 
276     /**
277      * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
278      * @see #addTouchHistory(MotionEvent)
279      */
280     private void clearTouchHistory() {
281         mTouchUpper = Float.NaN;
282         mTouchLower = Float.NaN;
283         mTouchHistoryLastAccepted = Float.NaN;
284         mTouchHistoryDirection = 0;
285         mTouchHistoryLastAcceptedTime = 0;
286     }
287 
288     /**
289      * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
290      * when appropriate.
291      *
292      * <p>Applications should pass a complete and consistent event stream to this method.
293      * A complete and consistent event stream involves all MotionEvents from the initial
294      * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
295      *
296      * @param event The event to process
297      * @return true if the event was processed and the detector wants to receive the
298      *         rest of the MotionEvents in this event stream.
299      */
300     public boolean onTouchEvent(MotionEvent event) {
301         if (mInputEventConsistencyVerifier != null) {
302             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
303         }
304 
305         mCurrTime = event.getEventTime();
306 
307         final int action = event.getActionMasked();
308 
309         // Forward the event to check for double tap gesture
310         if (mQuickScaleEnabled) {
311             mGestureDetector.onTouchEvent(event);
312         }
313 
314         final boolean streamComplete = action == MotionEvent.ACTION_UP ||
315                 action == MotionEvent.ACTION_CANCEL;
316 
317         if (action == MotionEvent.ACTION_DOWN || streamComplete) {
318             // Reset any scale in progress with the listener.
319             // If it's an ACTION_DOWN we're beginning a new event stream.
320             // This means the app probably didn't give us all the events. Shame on it.
321             if (mInProgress) {
322                 mListener.onScaleEnd(this);
323                 mInProgress = false;
324                 mInitialSpan = 0;
325                 mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
326             } else if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS && streamComplete) {
327                 mInProgress = false;
328                 mInitialSpan = 0;
329                 mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
330             }
331 
332             if (streamComplete) {
333                 clearTouchHistory();
334                 return true;
335             }
336         }
337 
338         final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
339                 action == MotionEvent.ACTION_POINTER_UP ||
340                 action == MotionEvent.ACTION_POINTER_DOWN;
341 
342 
343         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
344         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
345 
346         // Determine focal point
347         float sumX = 0, sumY = 0;
348         final int count = event.getPointerCount();
349         final int div = pointerUp ? count - 1 : count;
350         final float focusX;
351         final float focusY;
352         if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) {
353             // In double tap mode, the focal pt is always where the double tap
354             // gesture started
355             focusX = mDoubleTapEvent.getX();
356             focusY = mDoubleTapEvent.getY();
357             if (event.getY() < focusY) {
358                 mEventBeforeOrAboveStartingGestureEvent = true;
359             } else {
360                 mEventBeforeOrAboveStartingGestureEvent = false;
361             }
362         } else {
363             for (int i = 0; i < count; i++) {
364                 if (skipIndex == i) continue;
365                 sumX += event.getX(i);
366                 sumY += event.getY(i);
367             }
368 
369             focusX = sumX / div;
370             focusY = sumY / div;
371         }
372 
373         addTouchHistory(event);
374 
375         // Determine average deviation from focal point
376         float devSumX = 0, devSumY = 0;
377         for (int i = 0; i < count; i++) {
378             if (skipIndex == i) continue;
379 
380             // Convert the resulting diameter into a radius.
381             final float touchSize = mTouchHistoryLastAccepted / 2;
382             devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
383             devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
384         }
385         final float devX = devSumX / div;
386         final float devY = devSumY / div;
387 
388         // Span is the average distance between touch points through the focal point;
389         // i.e. the diameter of the circle with a radius of the average deviation from
390         // the focal point.
391         final float spanX = devX * 2;
392         final float spanY = devY * 2;
393         final float span;
394         if (inDoubleTapMode()) {
395             span = spanY;
396         } else {
397             span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
398         }
399 
400         // Dispatch begin/end events as needed.
401         // If the configuration changes, notify the app to reset its current state by beginning
402         // a fresh scale event stream.
403         final boolean wasInProgress = mInProgress;
404         mFocusX = focusX;
405         mFocusY = focusY;
406         if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) {
407             mListener.onScaleEnd(this);
408             mInProgress = false;
409             mInitialSpan = span;
410             mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
411         }
412         if (configChanged) {
413             mPrevSpanX = mCurrSpanX = spanX;
414             mPrevSpanY = mCurrSpanY = spanY;
415             mInitialSpan = mPrevSpan = mCurrSpan = span;
416         }
417 
418         final int minSpan = inDoubleTapMode() ? mSpanSlop : mMinSpan;
419         if (!mInProgress && span >=  minSpan &&
420                 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
421             mPrevSpanX = mCurrSpanX = spanX;
422             mPrevSpanY = mCurrSpanY = spanY;
423             mPrevSpan = mCurrSpan = span;
424             mPrevTime = mCurrTime;
425             mInProgress = mListener.onScaleBegin(this);
426         }
427 
428         // Handle motion; focal point and span/scale factor are changing.
429         if (action == MotionEvent.ACTION_MOVE) {
430             mCurrSpanX = spanX;
431             mCurrSpanY = spanY;
432             mCurrSpan = span;
433 
434             boolean updatePrev = true;
435 
436             if (mInProgress) {
437                 updatePrev = mListener.onScale(this);
438             }
439 
440             if (updatePrev) {
441                 mPrevSpanX = mCurrSpanX;
442                 mPrevSpanY = mCurrSpanY;
443                 mPrevSpan = mCurrSpan;
444                 mPrevTime = mCurrTime;
445             }
446         }
447 
448         return true;
449     }
450 
451 
452     private boolean inDoubleTapMode() {
453         return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS;
454     }
455 
456     /**
457      * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
458      * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
459      * if the app targets API 19 and newer.
460      * @param scales true to enable quick scaling, false to disable
461      */
462     public void setQuickScaleEnabled(boolean scales) {
463         mQuickScaleEnabled = scales;
464         if (mQuickScaleEnabled && mGestureDetector == null) {
465             GestureDetector.SimpleOnGestureListener gestureListener =
466                     new GestureDetector.SimpleOnGestureListener() {
467                         @Override
468                         public boolean onDoubleTap(MotionEvent e) {
469                             // Double tap: start watching for a swipe
470                             mDoubleTapEvent = e;
471                             mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS;
472                             return true;
473                         }
474                     };
475             mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
476         }
477     }
478 
479   /**
480    * Return whether the quick scale gesture, in which the user performs a double tap followed by a
481    * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
482    */
483     public boolean isQuickScaleEnabled() {
484         return mQuickScaleEnabled;
485     }
486 
487     /**
488      * Returns {@code true} if a scale gesture is in progress.
489      */
490     public boolean isInProgress() {
491         return mInProgress;
492     }
493 
494     /**
495      * Get the X coordinate of the current gesture's focal point.
496      * If a gesture is in progress, the focal point is between
497      * each of the pointers forming the gesture.
498      *
499      * If {@link #isInProgress()} would return false, the result of this
500      * function is undefined.
501      *
502      * @return X coordinate of the focal point in pixels.
503      */
504     public float getFocusX() {
505         return mFocusX;
506     }
507 
508     /**
509      * Get the Y coordinate of the current gesture's focal point.
510      * If a gesture is in progress, the focal point is between
511      * each of the pointers forming the gesture.
512      *
513      * If {@link #isInProgress()} would return false, the result of this
514      * function is undefined.
515      *
516      * @return Y coordinate of the focal point in pixels.
517      */
518     public float getFocusY() {
519         return mFocusY;
520     }
521 
522     /**
523      * Return the average 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      */
528     public float getCurrentSpan() {
529         return mCurrSpan;
530     }
531 
532     /**
533      * Return the average X distance between each of the pointers forming the
534      * gesture in progress through the focal point.
535      *
536      * @return Distance between pointers in pixels.
537      */
538     public float getCurrentSpanX() {
539         return mCurrSpanX;
540     }
541 
542     /**
543      * Return the average Y distance between each of the pointers forming the
544      * gesture in progress through the focal point.
545      *
546      * @return Distance between pointers in pixels.
547      */
548     public float getCurrentSpanY() {
549         return mCurrSpanY;
550     }
551 
552     /**
553      * Return the previous average 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      */
558     public float getPreviousSpan() {
559         return mPrevSpan;
560     }
561 
562     /**
563      * Return the previous average X distance between each of the pointers forming the
564      * gesture in progress through the focal point.
565      *
566      * @return Previous distance between pointers in pixels.
567      */
568     public float getPreviousSpanX() {
569         return mPrevSpanX;
570     }
571 
572     /**
573      * Return the previous average Y distance between each of the pointers forming the
574      * gesture in progress through the focal point.
575      *
576      * @return Previous distance between pointers in pixels.
577      */
578     public float getPreviousSpanY() {
579         return mPrevSpanY;
580     }
581 
582     /**
583      * Return the scaling factor from the previous scale event to the current
584      * event. This value is defined as
585      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
586      *
587      * @return The current scaling factor.
588      */
589     public float getScaleFactor() {
590         if (inDoubleTapMode()) {
591             // Drag is moving up; the further away from the gesture
592             // start, the smaller the span should be, the closer,
593             // the larger the span, and therefore the larger the scale
594             final boolean scaleUp =
595                     (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
596                     (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
597             final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
598             return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
599         }
600         return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
601     }
602 
603     /**
604      * Return the time difference in milliseconds between the previous
605      * accepted scaling event and the current scaling event.
606      *
607      * @return Time difference since the last scaling event in milliseconds.
608      */
609     public long getTimeDelta() {
610         return mCurrTime - mPrevTime;
611     }
612 
613     /**
614      * Return the event time of the current event being processed.
615      *
616      * @return Current event time in milliseconds.
617      */
618     public long getEventTime() {
619         return mCurrTime;
620     }
621 }