• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.os.SystemClock;
22 import android.util.FloatMath;
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     private final OnScaleGestureListener mListener;
127 
128     private float mFocusX;
129     private float mFocusY;
130 
131     private float mCurrSpan;
132     private float mPrevSpan;
133     private float mInitialSpan;
134     private float mCurrSpanX;
135     private float mCurrSpanY;
136     private float mPrevSpanX;
137     private float mPrevSpanY;
138     private long mCurrTime;
139     private long mPrevTime;
140     private boolean mInProgress;
141     private int mSpanSlop;
142     private int mMinSpan;
143 
144     // Bounds for recently seen values
145     private float mTouchUpper;
146     private float mTouchLower;
147     private float mTouchHistoryLastAccepted;
148     private int mTouchHistoryDirection;
149     private long mTouchHistoryLastAcceptedTime;
150     private int mTouchMinMajor;
151 
152     private static final long TOUCH_STABILIZE_TIME = 128; // ms
153     private static final int TOUCH_MIN_MAJOR = 48; // dp
154 
155     /**
156      * Consistency verifier for debugging purposes.
157      */
158     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
159             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
160                     new InputEventConsistencyVerifier(this, 0) : null;
161 
ScaleGestureDetector(Context context, OnScaleGestureListener listener)162     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
163         mContext = context;
164         mListener = listener;
165         mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
166 
167         final Resources res = context.getResources();
168         mTouchMinMajor = res.getDimensionPixelSize(
169                 com.android.internal.R.dimen.config_minScalingTouchMajor);
170         mMinSpan = res.getDimensionPixelSize(
171                 com.android.internal.R.dimen.config_minScalingSpan);
172     }
173 
174     /**
175      * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
176      * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
177      * @param ev MotionEvent to add to the ongoing history
178      */
addTouchHistory(MotionEvent ev)179     private void addTouchHistory(MotionEvent ev) {
180         final long currentTime = SystemClock.uptimeMillis();
181         final int count = ev.getPointerCount();
182         boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
183         float total = 0;
184         int sampleCount = 0;
185         for (int i = 0; i < count; i++) {
186             final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
187             final int historySize = ev.getHistorySize();
188             final int pointerSampleCount = historySize + 1;
189             for (int h = 0; h < pointerSampleCount; h++) {
190                 float major;
191                 if (h < historySize) {
192                     major = ev.getHistoricalTouchMajor(i, h);
193                 } else {
194                     major = ev.getTouchMajor(i);
195                 }
196                 if (major < mTouchMinMajor) major = mTouchMinMajor;
197                 total += major;
198 
199                 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
200                     mTouchUpper = major;
201                 }
202                 if (Float.isNaN(mTouchLower) || major < mTouchLower) {
203                     mTouchLower = major;
204                 }
205 
206                 if (hasLastAccepted) {
207                     final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
208                     if (directionSig != mTouchHistoryDirection ||
209                             (directionSig == 0 && mTouchHistoryDirection == 0)) {
210                         mTouchHistoryDirection = directionSig;
211                         final long time = h < historySize ? ev.getHistoricalEventTime(h)
212                                 : ev.getEventTime();
213                         mTouchHistoryLastAcceptedTime = time;
214                         accept = false;
215                     }
216                 }
217             }
218             sampleCount += pointerSampleCount;
219         }
220 
221         final float avg = total / sampleCount;
222 
223         if (accept) {
224             float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
225             mTouchUpper = (mTouchUpper + newAccepted) / 2;
226             mTouchLower = (mTouchLower + newAccepted) / 2;
227             mTouchHistoryLastAccepted = newAccepted;
228             mTouchHistoryDirection = 0;
229             mTouchHistoryLastAcceptedTime = ev.getEventTime();
230         }
231     }
232 
233     /**
234      * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
235      * @see #addTouchHistory(MotionEvent)
236      */
237     private void clearTouchHistory() {
238         mTouchUpper = Float.NaN;
239         mTouchLower = Float.NaN;
240         mTouchHistoryLastAccepted = Float.NaN;
241         mTouchHistoryDirection = 0;
242         mTouchHistoryLastAcceptedTime = 0;
243     }
244 
245     /**
246      * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
247      * when appropriate.
248      *
249      * <p>Applications should pass a complete and consistent event stream to this method.
250      * A complete and consistent event stream involves all MotionEvents from the initial
251      * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
252      *
253      * @param event The event to process
254      * @return true if the event was processed and the detector wants to receive the
255      *         rest of the MotionEvents in this event stream.
256      */
257     public boolean onTouchEvent(MotionEvent event) {
258         if (mInputEventConsistencyVerifier != null) {
259             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
260         }
261 
262         mCurrTime = event.getEventTime();
263 
264         final int action = event.getActionMasked();
265 
266         final boolean streamComplete = action == MotionEvent.ACTION_UP ||
267                 action == MotionEvent.ACTION_CANCEL;
268         if (action == MotionEvent.ACTION_DOWN || streamComplete) {
269             // Reset any scale in progress with the listener.
270             // If it's an ACTION_DOWN we're beginning a new event stream.
271             // This means the app probably didn't give us all the events. Shame on it.
272             if (mInProgress) {
273                 mListener.onScaleEnd(this);
274                 mInProgress = false;
275                 mInitialSpan = 0;
276             }
277 
278             if (streamComplete) {
279                 clearTouchHistory();
280                 return true;
281             }
282         }
283 
284         final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
285                 action == MotionEvent.ACTION_POINTER_UP ||
286                 action == MotionEvent.ACTION_POINTER_DOWN;
287         final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
288         final int skipIndex = pointerUp ? event.getActionIndex() : -1;
289 
290         // Determine focal point
291         float sumX = 0, sumY = 0;
292         final int count = event.getPointerCount();
293         for (int i = 0; i < count; i++) {
294             if (skipIndex == i) continue;
295             sumX += event.getX(i);
296             sumY += event.getY(i);
297         }
298         final int div = pointerUp ? count - 1 : count;
299         final float focusX = sumX / div;
300         final float focusY = sumY / div;
301 
302 
303         addTouchHistory(event);
304 
305         // Determine average deviation from focal point
306         float devSumX = 0, devSumY = 0;
307         for (int i = 0; i < count; i++) {
308             if (skipIndex == i) continue;
309 
310             // Convert the resulting diameter into a radius.
311             final float touchSize = mTouchHistoryLastAccepted / 2;
312             devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
313             devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
314         }
315         final float devX = devSumX / div;
316         final float devY = devSumY / div;
317 
318         // Span is the average distance between touch points through the focal point;
319         // i.e. the diameter of the circle with a radius of the average deviation from
320         // the focal point.
321         final float spanX = devX * 2;
322         final float spanY = devY * 2;
323         final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
324 
325         // Dispatch begin/end events as needed.
326         // If the configuration changes, notify the app to reset its current state by beginning
327         // a fresh scale event stream.
328         final boolean wasInProgress = mInProgress;
329         mFocusX = focusX;
330         mFocusY = focusY;
331         if (mInProgress && (span < mMinSpan || configChanged)) {
332             mListener.onScaleEnd(this);
333             mInProgress = false;
334             mInitialSpan = span;
335         }
336         if (configChanged) {
337             mPrevSpanX = mCurrSpanX = spanX;
338             mPrevSpanY = mCurrSpanY = spanY;
339             mInitialSpan = mPrevSpan = mCurrSpan = span;
340         }
341         if (!mInProgress && span >= mMinSpan &&
342                 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
343             mPrevSpanX = mCurrSpanX = spanX;
344             mPrevSpanY = mCurrSpanY = spanY;
345             mPrevSpan = mCurrSpan = span;
346             mPrevTime = mCurrTime;
347             mInProgress = mListener.onScaleBegin(this);
348         }
349 
350         // Handle motion; focal point and span/scale factor are changing.
351         if (action == MotionEvent.ACTION_MOVE) {
352             mCurrSpanX = spanX;
353             mCurrSpanY = spanY;
354             mCurrSpan = span;
355 
356             boolean updatePrev = true;
357             if (mInProgress) {
358                 updatePrev = mListener.onScale(this);
359             }
360 
361             if (updatePrev) {
362                 mPrevSpanX = mCurrSpanX;
363                 mPrevSpanY = mCurrSpanY;
364                 mPrevSpan = mCurrSpan;
365                 mPrevTime = mCurrTime;
366             }
367         }
368 
369         return true;
370     }
371 
372     /**
373      * Returns {@code true} if a scale gesture is in progress.
374      */
375     public boolean isInProgress() {
376         return mInProgress;
377     }
378 
379     /**
380      * Get the X coordinate of the current gesture's focal point.
381      * If a gesture is in progress, the focal point is between
382      * each of the pointers forming the gesture.
383      *
384      * If {@link #isInProgress()} would return false, the result of this
385      * function is undefined.
386      *
387      * @return X coordinate of the focal point in pixels.
388      */
389     public float getFocusX() {
390         return mFocusX;
391     }
392 
393     /**
394      * Get the Y coordinate of the current gesture's focal point.
395      * If a gesture is in progress, the focal point is between
396      * each of the pointers forming the gesture.
397      *
398      * If {@link #isInProgress()} would return false, the result of this
399      * function is undefined.
400      *
401      * @return Y coordinate of the focal point in pixels.
402      */
403     public float getFocusY() {
404         return mFocusY;
405     }
406 
407     /**
408      * Return the average distance between each of the pointers forming the
409      * gesture in progress through the focal point.
410      *
411      * @return Distance between pointers in pixels.
412      */
413     public float getCurrentSpan() {
414         return mCurrSpan;
415     }
416 
417     /**
418      * Return the average X distance between each of the pointers forming the
419      * gesture in progress through the focal point.
420      *
421      * @return Distance between pointers in pixels.
422      */
423     public float getCurrentSpanX() {
424         return mCurrSpanX;
425     }
426 
427     /**
428      * Return the average Y distance between each of the pointers forming the
429      * gesture in progress through the focal point.
430      *
431      * @return Distance between pointers in pixels.
432      */
433     public float getCurrentSpanY() {
434         return mCurrSpanY;
435     }
436 
437     /**
438      * Return the previous average distance between each of the pointers forming the
439      * gesture in progress through the focal point.
440      *
441      * @return Previous distance between pointers in pixels.
442      */
443     public float getPreviousSpan() {
444         return mPrevSpan;
445     }
446 
447     /**
448      * Return the previous average X distance between each of the pointers forming the
449      * gesture in progress through the focal point.
450      *
451      * @return Previous distance between pointers in pixels.
452      */
453     public float getPreviousSpanX() {
454         return mPrevSpanX;
455     }
456 
457     /**
458      * Return the previous average Y distance between each of the pointers forming the
459      * gesture in progress through the focal point.
460      *
461      * @return Previous distance between pointers in pixels.
462      */
463     public float getPreviousSpanY() {
464         return mPrevSpanY;
465     }
466 
467     /**
468      * Return the scaling factor from the previous scale event to the current
469      * event. This value is defined as
470      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
471      *
472      * @return The current scaling factor.
473      */
474     public float getScaleFactor() {
475         return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
476     }
477 
478     /**
479      * Return the time difference in milliseconds between the previous
480      * accepted scaling event and the current scaling event.
481      *
482      * @return Time difference since the last scaling event in milliseconds.
483      */
484     public long getTimeDelta() {
485         return mCurrTime - mPrevTime;
486     }
487 
488     /**
489      * Return the event time of the current event being processed.
490      *
491      * @return Current event time in milliseconds.
492      */
493     public long getEventTime() {
494         return mCurrTime;
495     }
496 }
497