ScaleGestureDetector.java revision 076f17375b9530d40767b990016a83a9fcd18bf2
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
17package android.view;
18
19import android.content.Context;
20import android.util.DisplayMetrics;
21import android.util.FloatMath;
22import 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 */
39public 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         */
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         */
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         */
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
111        public boolean onScale(ScaleGestureDetector detector) {
112            return false;
113        }
114
115        public boolean onScaleBegin(ScaleGestureDetector detector) {
116            return true;
117        }
118
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
169    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
170        mContext = context;
171        mListener = listener;
172    }
173
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
563    public long getEventTime() {
564        return mCurrEvent.getEventTime();
565    }
566}
567