• 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.util.DisplayMetrics;
21 import android.util.FloatMath;
22 import android.util.Log;
23 
24 /**
25  * Detects transformation gestures involving more than one pointer ("multitouch")
26  * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
27  * callback will notify users when a particular gesture event has occurred.
28  * This class should only be used with {@link MotionEvent}s reported via touch.
29  *
30  * To use this class:
31  * <ul>
32  *  <li>Create an instance of the {@code ScaleGestureDetector} for your
33  *      {@link View}
34  *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
35  *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
36  *          callback will be executed when the events occur.
37  * </ul>
38  */
39 public class ScaleGestureDetector {
40     private static final String TAG = "ScaleGestureDetector";
41 
42     /**
43      * The listener for receiving notifications when gestures occur.
44      * If you want to listen for all the different gestures then implement
45      * this interface. If you only want to listen for a subset it might
46      * be easier to extend {@link SimpleOnScaleGestureListener}.
47      *
48      * An application will receive events in the following order:
49      * <ul>
50      *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
51      *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
52      *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
53      * </ul>
54      */
55     public interface OnScaleGestureListener {
56         /**
57          * Responds to scaling events for a gesture in progress.
58          * Reported by pointer motion.
59          *
60          * @param detector The detector reporting the event - use this to
61          *          retrieve extended info about event state.
62          * @return Whether or not the detector should consider this event
63          *          as handled. If an event was not handled, the detector
64          *          will continue to accumulate movement until an event is
65          *          handled. This can be useful if an application, for example,
66          *          only wants to update scaling factors if the change is
67          *          greater than 0.01.
68          */
onScale(ScaleGestureDetector detector)69         public boolean onScale(ScaleGestureDetector detector);
70 
71         /**
72          * Responds to the beginning of a scaling gesture. Reported by
73          * new pointers going down.
74          *
75          * @param detector The detector reporting the event - use this to
76          *          retrieve extended info about event state.
77          * @return Whether or not the detector should continue recognizing
78          *          this gesture. For example, if a gesture is beginning
79          *          with a focal point outside of a region where it makes
80          *          sense, onScaleBegin() may return false to ignore the
81          *          rest of the gesture.
82          */
onScaleBegin(ScaleGestureDetector detector)83         public boolean onScaleBegin(ScaleGestureDetector detector);
84 
85         /**
86          * Responds to the end of a scale gesture. Reported by existing
87          * pointers going up.
88          *
89          * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
90          * and {@link ScaleGestureDetector#getFocusY()} will return the location
91          * of the pointer remaining on the screen.
92          *
93          * @param detector The detector reporting the event - use this to
94          *          retrieve extended info about event state.
95          */
onScaleEnd(ScaleGestureDetector detector)96         public void onScaleEnd(ScaleGestureDetector detector);
97     }
98 
99     /**
100      * A convenience class to extend when you only want to listen for a subset
101      * of scaling-related events. This implements all methods in
102      * {@link OnScaleGestureListener} but does nothing.
103      * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
104      * {@code false} so that a subclass can retrieve the accumulated scale
105      * factor in an overridden onScaleEnd.
106      * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
107      * {@code true}.
108      */
109     public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
110 
onScale(ScaleGestureDetector detector)111         public boolean onScale(ScaleGestureDetector detector) {
112             return false;
113         }
114 
onScaleBegin(ScaleGestureDetector detector)115         public boolean onScaleBegin(ScaleGestureDetector detector) {
116             return true;
117         }
118 
onScaleEnd(ScaleGestureDetector detector)119         public void onScaleEnd(ScaleGestureDetector detector) {
120             // Intentionally empty
121         }
122     }
123 
124     /**
125      * This value is the threshold ratio between our previous combined pressure
126      * and the current combined pressure. We will only fire an onScale event if
127      * the computed ratio between the current and previous event pressures is
128      * greater than this value. When pressure decreases rapidly between events
129      * the position values can often be imprecise, as it usually indicates
130      * that the user is in the process of lifting a pointer off of the device.
131      * Its value was tuned experimentally.
132      */
133     private static final float PRESSURE_THRESHOLD = 0.67f;
134 
135     private final Context mContext;
136     private final OnScaleGestureListener mListener;
137     private boolean mGestureInProgress;
138 
139     private MotionEvent mPrevEvent;
140     private MotionEvent mCurrEvent;
141 
142     private float mFocusX;
143     private float mFocusY;
144     private float mPrevFingerDiffX;
145     private float mPrevFingerDiffY;
146     private float mCurrFingerDiffX;
147     private float mCurrFingerDiffY;
148     private float mCurrLen;
149     private float mPrevLen;
150     private float mScaleFactor;
151     private float mCurrPressure;
152     private float mPrevPressure;
153     private long mTimeDelta;
154 
155     private boolean mInvalidGesture;
156 
157     // Pointer IDs currently responsible for the two fingers controlling the gesture
158     private int mActiveId0;
159     private int mActiveId1;
160     private boolean mActive0MostRecent;
161 
162     /**
163      * Consistency verifier for debugging purposes.
164      */
165     private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
166             InputEventConsistencyVerifier.isInstrumentationEnabled() ?
167                     new InputEventConsistencyVerifier(this, 0) : null;
168 
ScaleGestureDetector(Context context, OnScaleGestureListener listener)169     public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
170         mContext = context;
171         mListener = listener;
172     }
173 
onTouchEvent(MotionEvent event)174     public boolean onTouchEvent(MotionEvent event) {
175         if (mInputEventConsistencyVerifier != null) {
176             mInputEventConsistencyVerifier.onTouchEvent(event, 0);
177         }
178 
179         final int action = event.getActionMasked();
180 
181         if (action == MotionEvent.ACTION_DOWN) {
182             reset(); // Start fresh
183         }
184 
185         boolean handled = true;
186         if (mInvalidGesture) {
187             handled = false;
188         } else if (!mGestureInProgress) {
189             switch (action) {
190                 case MotionEvent.ACTION_DOWN: {
191                     mActiveId0 = event.getPointerId(0);
192                     mActive0MostRecent = true;
193                 }
194                 break;
195 
196                 case MotionEvent.ACTION_UP:
197                     reset();
198                     break;
199 
200                 case MotionEvent.ACTION_POINTER_DOWN: {
201                     // We have a new multi-finger gesture
202                     if (mPrevEvent != null) mPrevEvent.recycle();
203                     mPrevEvent = MotionEvent.obtain(event);
204                     mTimeDelta = 0;
205 
206                     int index1 = event.getActionIndex();
207                     int index0 = event.findPointerIndex(mActiveId0);
208                     mActiveId1 = event.getPointerId(index1);
209                     if (index0 < 0 || index0 == index1) {
210                         // Probably someone sending us a broken event stream.
211                         index0 = findNewActiveIndex(event, mActiveId1, -1);
212                         mActiveId0 = event.getPointerId(index0);
213                     }
214                     mActive0MostRecent = false;
215 
216                     setContext(event);
217 
218                     mGestureInProgress = mListener.onScaleBegin(this);
219                     break;
220                 }
221             }
222         } else {
223             // Transform gesture in progress - attempt to handle it
224             switch (action) {
225                 case MotionEvent.ACTION_POINTER_DOWN: {
226                     // End the old gesture and begin a new one with the most recent two fingers.
227                     mListener.onScaleEnd(this);
228                     final int oldActive0 = mActiveId0;
229                     final int oldActive1 = mActiveId1;
230                     reset();
231 
232                     mPrevEvent = MotionEvent.obtain(event);
233                     mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1;
234                     mActiveId1 = event.getPointerId(event.getActionIndex());
235                     mActive0MostRecent = false;
236 
237                     int index0 = event.findPointerIndex(mActiveId0);
238                     if (index0 < 0 || mActiveId0 == mActiveId1) {
239                         // Probably someone sending us a broken event stream.
240                         Log.e(TAG, "Got " + MotionEvent.actionToString(action) +
241                                 " with bad state while a gesture was in progress. " +
242                                 "Did you forget to pass an event to " +
243                                 "ScaleGestureDetector#onTouchEvent?");
244                         index0 = findNewActiveIndex(event, mActiveId1, -1);
245                         mActiveId0 = event.getPointerId(index0);
246                     }
247 
248                     setContext(event);
249 
250                     mGestureInProgress = mListener.onScaleBegin(this);
251                 }
252                 break;
253 
254                 case MotionEvent.ACTION_POINTER_UP: {
255                     final int pointerCount = event.getPointerCount();
256                     final int actionIndex = event.getActionIndex();
257                     final int actionId = event.getPointerId(actionIndex);
258 
259                     boolean gestureEnded = false;
260                     if (pointerCount > 2) {
261                         if (actionId == mActiveId0) {
262                             final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex);
263                             if (newIndex >= 0) {
264                                 mListener.onScaleEnd(this);
265                                 mActiveId0 = event.getPointerId(newIndex);
266                                 mActive0MostRecent = true;
267                                 mPrevEvent = MotionEvent.obtain(event);
268                                 setContext(event);
269                                 mGestureInProgress = mListener.onScaleBegin(this);
270                             } else {
271                                 gestureEnded = true;
272                             }
273                         } else if (actionId == mActiveId1) {
274                             final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex);
275                             if (newIndex >= 0) {
276                                 mListener.onScaleEnd(this);
277                                 mActiveId1 = event.getPointerId(newIndex);
278                                 mActive0MostRecent = false;
279                                 mPrevEvent = MotionEvent.obtain(event);
280                                 setContext(event);
281                                 mGestureInProgress = mListener.onScaleBegin(this);
282                             } else {
283                                 gestureEnded = true;
284                             }
285                         }
286                         mPrevEvent.recycle();
287                         mPrevEvent = MotionEvent.obtain(event);
288                         setContext(event);
289                     } else {
290                         gestureEnded = true;
291                     }
292 
293                     if (gestureEnded) {
294                         // Gesture ended
295                         setContext(event);
296 
297                         // Set focus point to the remaining finger
298                         final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0;
299                         final int index = event.findPointerIndex(activeId);
300                         mFocusX = event.getX(index);
301                         mFocusY = event.getY(index);
302 
303                         mListener.onScaleEnd(this);
304                         reset();
305                         mActiveId0 = activeId;
306                         mActive0MostRecent = true;
307                     }
308                 }
309                 break;
310 
311                 case MotionEvent.ACTION_CANCEL:
312                     mListener.onScaleEnd(this);
313                     reset();
314                     break;
315 
316                 case MotionEvent.ACTION_UP:
317                     reset();
318                     break;
319 
320                 case MotionEvent.ACTION_MOVE: {
321                     setContext(event);
322 
323                     // Only accept the event if our relative pressure is within
324                     // a certain limit - this can help filter shaky data as a
325                     // finger is lifted.
326                     if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
327                         final boolean updatePrevious = mListener.onScale(this);
328 
329                         if (updatePrevious) {
330                             mPrevEvent.recycle();
331                             mPrevEvent = MotionEvent.obtain(event);
332                         }
333                     }
334                 }
335                 break;
336             }
337         }
338 
339         if (!handled && mInputEventConsistencyVerifier != null) {
340             mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
341         }
342         return handled;
343     }
344 
findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex)345     private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex) {
346         final int pointerCount = ev.getPointerCount();
347 
348         // It's ok if this isn't found and returns -1, it simply won't match.
349         final int otherActiveIndex = ev.findPointerIndex(otherActiveId);
350 
351         // Pick a new id and update tracking state.
352         for (int i = 0; i < pointerCount; i++) {
353             if (i != removedPointerIndex && i != otherActiveIndex) {
354                 return i;
355             }
356         }
357         return -1;
358     }
359 
setContext(MotionEvent curr)360     private void setContext(MotionEvent curr) {
361         if (mCurrEvent != null) {
362             mCurrEvent.recycle();
363         }
364         mCurrEvent = MotionEvent.obtain(curr);
365 
366         mCurrLen = -1;
367         mPrevLen = -1;
368         mScaleFactor = -1;
369 
370         final MotionEvent prev = mPrevEvent;
371 
372         final int prevIndex0 = prev.findPointerIndex(mActiveId0);
373         final int prevIndex1 = prev.findPointerIndex(mActiveId1);
374         final int currIndex0 = curr.findPointerIndex(mActiveId0);
375         final int currIndex1 = curr.findPointerIndex(mActiveId1);
376 
377         if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) {
378             mInvalidGesture = true;
379             Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable());
380             if (mGestureInProgress) {
381                 mListener.onScaleEnd(this);
382             }
383             return;
384         }
385 
386         final float px0 = prev.getX(prevIndex0);
387         final float py0 = prev.getY(prevIndex0);
388         final float px1 = prev.getX(prevIndex1);
389         final float py1 = prev.getY(prevIndex1);
390         final float cx0 = curr.getX(currIndex0);
391         final float cy0 = curr.getY(currIndex0);
392         final float cx1 = curr.getX(currIndex1);
393         final float cy1 = curr.getY(currIndex1);
394 
395         final float pvx = px1 - px0;
396         final float pvy = py1 - py0;
397         final float cvx = cx1 - cx0;
398         final float cvy = cy1 - cy0;
399         mPrevFingerDiffX = pvx;
400         mPrevFingerDiffY = pvy;
401         mCurrFingerDiffX = cvx;
402         mCurrFingerDiffY = cvy;
403 
404         mFocusX = cx0 + cvx * 0.5f;
405         mFocusY = cy0 + cvy * 0.5f;
406         mTimeDelta = curr.getEventTime() - prev.getEventTime();
407         mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1);
408         mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1);
409     }
410 
reset()411     private void reset() {
412         if (mPrevEvent != null) {
413             mPrevEvent.recycle();
414             mPrevEvent = null;
415         }
416         if (mCurrEvent != null) {
417             mCurrEvent.recycle();
418             mCurrEvent = null;
419         }
420         mGestureInProgress = false;
421         mActiveId0 = -1;
422         mActiveId1 = -1;
423         mInvalidGesture = false;
424     }
425 
426     /**
427      * Returns {@code true} if a two-finger scale gesture is in progress.
428      * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
429      */
isInProgress()430     public boolean isInProgress() {
431         return mGestureInProgress;
432     }
433 
434     /**
435      * Get the X coordinate of the current gesture's focal point.
436      * If a gesture is in progress, the focal point is directly between
437      * the two pointers forming the gesture.
438      * If a gesture is ending, the focal point is the location of the
439      * remaining pointer on the screen.
440      * If {@link #isInProgress()} would return false, the result of this
441      * function is undefined.
442      *
443      * @return X coordinate of the focal point in pixels.
444      */
getFocusX()445     public float getFocusX() {
446         return mFocusX;
447     }
448 
449     /**
450      * Get the Y coordinate of the current gesture's focal point.
451      * If a gesture is in progress, the focal point is directly between
452      * the two pointers forming the gesture.
453      * If a gesture is ending, the focal point is the location of the
454      * remaining pointer on the screen.
455      * If {@link #isInProgress()} would return false, the result of this
456      * function is undefined.
457      *
458      * @return Y coordinate of the focal point in pixels.
459      */
getFocusY()460     public float getFocusY() {
461         return mFocusY;
462     }
463 
464     /**
465      * Return the current distance between the two pointers forming the
466      * gesture in progress.
467      *
468      * @return Distance between pointers in pixels.
469      */
getCurrentSpan()470     public float getCurrentSpan() {
471         if (mCurrLen == -1) {
472             final float cvx = mCurrFingerDiffX;
473             final float cvy = mCurrFingerDiffY;
474             mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
475         }
476         return mCurrLen;
477     }
478 
479     /**
480      * Return the current x distance between the two pointers forming the
481      * gesture in progress.
482      *
483      * @return Distance between pointers in pixels.
484      */
getCurrentSpanX()485     public float getCurrentSpanX() {
486         return mCurrFingerDiffX;
487     }
488 
489     /**
490      * Return the current y distance between the two pointers forming the
491      * gesture in progress.
492      *
493      * @return Distance between pointers in pixels.
494      */
getCurrentSpanY()495     public float getCurrentSpanY() {
496         return mCurrFingerDiffY;
497     }
498 
499     /**
500      * Return the previous distance between the two pointers forming the
501      * gesture in progress.
502      *
503      * @return Previous distance between pointers in pixels.
504      */
getPreviousSpan()505     public float getPreviousSpan() {
506         if (mPrevLen == -1) {
507             final float pvx = mPrevFingerDiffX;
508             final float pvy = mPrevFingerDiffY;
509             mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
510         }
511         return mPrevLen;
512     }
513 
514     /**
515      * Return the previous x distance between the two pointers forming the
516      * gesture in progress.
517      *
518      * @return Previous distance between pointers in pixels.
519      */
getPreviousSpanX()520     public float getPreviousSpanX() {
521         return mPrevFingerDiffX;
522     }
523 
524     /**
525      * Return the previous y distance between the two pointers forming the
526      * gesture in progress.
527      *
528      * @return Previous distance between pointers in pixels.
529      */
getPreviousSpanY()530     public float getPreviousSpanY() {
531         return mPrevFingerDiffY;
532     }
533 
534     /**
535      * Return the scaling factor from the previous scale event to the current
536      * event. This value is defined as
537      * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
538      *
539      * @return The current scaling factor.
540      */
getScaleFactor()541     public float getScaleFactor() {
542         if (mScaleFactor == -1) {
543             mScaleFactor = getCurrentSpan() / getPreviousSpan();
544         }
545         return mScaleFactor;
546     }
547 
548     /**
549      * Return the time difference in milliseconds between the previous
550      * accepted scaling event and the current scaling event.
551      *
552      * @return Time difference since the last scaling event in milliseconds.
553      */
getTimeDelta()554     public long getTimeDelta() {
555         return mTimeDelta;
556     }
557 
558     /**
559      * Return the event time of the current event being processed.
560      *
561      * @return Current event time in milliseconds.
562      */
getEventTime()563     public long getEventTime() {
564         return mCurrEvent.getEventTime();
565     }
566 }
567