ScaleGestureDetector.java revision 9bccdb7d5c93e350337e707bc6edf3cd017b8f96
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;
22
23/**
24 * Detects transformation gestures involving more than one pointer ("multitouch")
25 * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
26 * callback will notify users when a particular gesture event has occurred.
27 * This class should only be used with {@link MotionEvent}s reported via touch.
28 *
29 * To use this class:
30 * <ul>
31 *  <li>Create an instance of the {@code ScaleGestureDetector} for your
32 *      {@link View}
33 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
34 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
35 *          callback will be executed when the events occur.
36 * </ul>
37 */
38public class ScaleGestureDetector {
39    /**
40     * The listener for receiving notifications when gestures occur.
41     * If you want to listen for all the different gestures then implement
42     * this interface. If you only want to listen for a subset it might
43     * be easier to extend {@link SimpleOnScaleGestureListener}.
44     *
45     * An application will receive events in the following order:
46     * <ul>
47     *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
48     *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
49     *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
50     * </ul>
51     */
52    public interface OnScaleGestureListener {
53        /**
54         * Responds to scaling events for a gesture in progress.
55         * Reported by pointer motion.
56         *
57         * @param detector The detector reporting the event - use this to
58         *          retrieve extended info about event state.
59         * @return Whether or not the detector should consider this event
60         *          as handled. If an event was not handled, the detector
61         *          will continue to accumulate movement until an event is
62         *          handled. This can be useful if an application, for example,
63         *          only wants to update scaling factors if the change is
64         *          greater than 0.01.
65         */
66        public boolean onScale(ScaleGestureDetector detector);
67
68        /**
69         * Responds to the beginning of a scaling gesture. Reported by
70         * new pointers going down.
71         *
72         * @param detector The detector reporting the event - use this to
73         *          retrieve extended info about event state.
74         * @return Whether or not the detector should continue recognizing
75         *          this gesture. For example, if a gesture is beginning
76         *          with a focal point outside of a region where it makes
77         *          sense, onScaleBegin() may return false to ignore the
78         *          rest of the gesture.
79         */
80        public boolean onScaleBegin(ScaleGestureDetector detector);
81
82        /**
83         * Responds to the end of a scale gesture. Reported by existing
84         * pointers going up.
85         *
86         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
87         * and {@link ScaleGestureDetector#getFocusY()} will return the location
88         * of the pointer remaining on the screen.
89         *
90         * @param detector The detector reporting the event - use this to
91         *          retrieve extended info about event state.
92         */
93        public void onScaleEnd(ScaleGestureDetector detector);
94    }
95
96    /**
97     * A convenience class to extend when you only want to listen for a subset
98     * of scaling-related events. This implements all methods in
99     * {@link OnScaleGestureListener} but does nothing.
100     * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
101     * {@code false} so that a subclass can retrieve the accumulated scale
102     * factor in an overridden onScaleEnd.
103     * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
104     * {@code true}.
105     */
106    public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
107
108        public boolean onScale(ScaleGestureDetector detector) {
109            return false;
110        }
111
112        public boolean onScaleBegin(ScaleGestureDetector detector) {
113            return true;
114        }
115
116        public void onScaleEnd(ScaleGestureDetector detector) {
117            // Intentionally empty
118        }
119    }
120
121    /**
122     * This value is the threshold ratio between our previous combined pressure
123     * and the current combined pressure. We will only fire an onScale event if
124     * the computed ratio between the current and previous event pressures is
125     * greater than this value. When pressure decreases rapidly between events
126     * the position values can often be imprecise, as it usually indicates
127     * that the user is in the process of lifting a pointer off of the device.
128     * Its value was tuned experimentally.
129     */
130    private static final float PRESSURE_THRESHOLD = 0.67f;
131
132    private final Context mContext;
133    private final OnScaleGestureListener mListener;
134    private boolean mGestureInProgress;
135
136    private MotionEvent mPrevEvent;
137    private MotionEvent mCurrEvent;
138
139    private float mFocusX;
140    private float mFocusY;
141    private float mPrevFingerDiffX;
142    private float mPrevFingerDiffY;
143    private float mCurrFingerDiffX;
144    private float mCurrFingerDiffY;
145    private float mCurrLen;
146    private float mPrevLen;
147    private float mScaleFactor;
148    private float mCurrPressure;
149    private float mPrevPressure;
150    private long mTimeDelta;
151
152    private final float mEdgeSlop;
153    private float mRightSlopEdge;
154    private float mBottomSlopEdge;
155    private boolean mSloppyGesture;
156
157    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
158        ViewConfiguration config = ViewConfiguration.get(context);
159        mContext = context;
160        mListener = listener;
161        mEdgeSlop = config.getScaledEdgeSlop();
162    }
163
164    public boolean onTouchEvent(MotionEvent event) {
165        final int action = event.getAction();
166        boolean handled = true;
167
168        if (!mGestureInProgress) {
169            switch (action & MotionEvent.ACTION_MASK) {
170            case MotionEvent.ACTION_POINTER_DOWN: {
171                // We have a new multi-finger gesture
172
173                // as orientation can change, query the metrics in touch down
174                DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
175                mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
176                mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
177
178                // Be paranoid in case we missed an event
179                reset();
180
181                mPrevEvent = MotionEvent.obtain(event);
182                mTimeDelta = 0;
183
184                setContext(event);
185
186                // Check if we have a sloppy gesture. If so, delay
187                // the beginning of the gesture until we're sure that's
188                // what the user wanted. Sloppy gestures can happen if the
189                // edge of the user's hand is touching the screen, for example.
190                final float edgeSlop = mEdgeSlop;
191                final float rightSlop = mRightSlopEdge;
192                final float bottomSlop = mBottomSlopEdge;
193                final float x0 = event.getRawX();
194                final float y0 = event.getRawY();
195                final float x1 = getRawX(event, 1);
196                final float y1 = getRawY(event, 1);
197
198                boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
199                        || x0 > rightSlop || y0 > bottomSlop;
200                boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
201                        || x1 > rightSlop || y1 > bottomSlop;
202
203                if (p0sloppy && p1sloppy) {
204                    mFocusX = -1;
205                    mFocusY = -1;
206                    mSloppyGesture = true;
207                } else if (p0sloppy) {
208                    mFocusX = event.getX(1);
209                    mFocusY = event.getY(1);
210                    mSloppyGesture = true;
211                } else if (p1sloppy) {
212                    mFocusX = event.getX(0);
213                    mFocusY = event.getY(0);
214                    mSloppyGesture = true;
215                } else {
216                    mGestureInProgress = mListener.onScaleBegin(this);
217                }
218            }
219            break;
220
221            case MotionEvent.ACTION_MOVE:
222                if (mSloppyGesture) {
223                    // Initiate sloppy gestures if we've moved outside of the slop area.
224                    final float edgeSlop = mEdgeSlop;
225                    final float rightSlop = mRightSlopEdge;
226                    final float bottomSlop = mBottomSlopEdge;
227                    final float x0 = event.getRawX();
228                    final float y0 = event.getRawY();
229                    final float x1 = getRawX(event, 1);
230                    final float y1 = getRawY(event, 1);
231
232                    boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
233                    || x0 > rightSlop || y0 > bottomSlop;
234                    boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
235                    || x1 > rightSlop || y1 > bottomSlop;
236
237                    if(p0sloppy && p1sloppy) {
238                        mFocusX = -1;
239                        mFocusY = -1;
240                    } else if (p0sloppy) {
241                        mFocusX = event.getX(1);
242                        mFocusY = event.getY(1);
243                    } else if (p1sloppy) {
244                        mFocusX = event.getX(0);
245                        mFocusY = event.getY(0);
246                    } else {
247                        mSloppyGesture = false;
248                        mGestureInProgress = mListener.onScaleBegin(this);
249                    }
250                }
251                break;
252
253            case MotionEvent.ACTION_POINTER_UP:
254                if (mSloppyGesture) {
255                    // Set focus point to the remaining finger
256                    int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
257                            >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0;
258                    mFocusX = event.getX(id);
259                    mFocusY = event.getY(id);
260                }
261                break;
262            }
263        } else {
264            // Transform gesture in progress - attempt to handle it
265            switch (action & MotionEvent.ACTION_MASK) {
266                case MotionEvent.ACTION_POINTER_UP:
267                    // Gesture ended
268                    setContext(event);
269
270                    // Set focus point to the remaining finger
271                    int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK)
272                            >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0;
273                    mFocusX = event.getX(id);
274                    mFocusY = event.getY(id);
275
276                    if (!mSloppyGesture) {
277                        mListener.onScaleEnd(this);
278                    }
279
280                    reset();
281                    break;
282
283                case MotionEvent.ACTION_CANCEL:
284                    if (!mSloppyGesture) {
285                        mListener.onScaleEnd(this);
286                    }
287
288                    reset();
289                    break;
290
291                case MotionEvent.ACTION_MOVE:
292                    setContext(event);
293
294                    // Only accept the event if our relative pressure is within
295                    // a certain limit - this can help filter shaky data as a
296                    // finger is lifted.
297                    if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
298                        final boolean updatePrevious = mListener.onScale(this);
299
300                        if (updatePrevious) {
301                            mPrevEvent.recycle();
302                            mPrevEvent = MotionEvent.obtain(event);
303                        }
304                    }
305                    break;
306            }
307        }
308        return handled;
309    }
310
311    /**
312     * MotionEvent has no getRawX(int) method; simulate it pending future API approval.
313     */
314    private static float getRawX(MotionEvent event, int pointerIndex) {
315        float offset = event.getRawX() - event.getX();
316        return event.getX(pointerIndex) + offset;
317    }
318
319    /**
320     * MotionEvent has no getRawY(int) method; simulate it pending future API approval.
321     */
322    private static float getRawY(MotionEvent event, int pointerIndex) {
323        float offset = event.getRawY() - event.getY();
324        return event.getY(pointerIndex) + offset;
325    }
326
327    private void setContext(MotionEvent curr) {
328        if (mCurrEvent != null) {
329            mCurrEvent.recycle();
330        }
331        mCurrEvent = MotionEvent.obtain(curr);
332
333        mCurrLen = -1;
334        mPrevLen = -1;
335        mScaleFactor = -1;
336
337        final MotionEvent prev = mPrevEvent;
338
339        final float px0 = prev.getX(0);
340        final float py0 = prev.getY(0);
341        final float px1 = prev.getX(1);
342        final float py1 = prev.getY(1);
343        final float cx0 = curr.getX(0);
344        final float cy0 = curr.getY(0);
345        final float cx1 = curr.getX(1);
346        final float cy1 = curr.getY(1);
347
348        final float pvx = px1 - px0;
349        final float pvy = py1 - py0;
350        final float cvx = cx1 - cx0;
351        final float cvy = cy1 - cy0;
352        mPrevFingerDiffX = pvx;
353        mPrevFingerDiffY = pvy;
354        mCurrFingerDiffX = cvx;
355        mCurrFingerDiffY = cvy;
356
357        mFocusX = cx0 + cvx * 0.5f;
358        mFocusY = cy0 + cvy * 0.5f;
359        mTimeDelta = curr.getEventTime() - prev.getEventTime();
360        mCurrPressure = curr.getPressure(0) + curr.getPressure(1);
361        mPrevPressure = prev.getPressure(0) + prev.getPressure(1);
362    }
363
364    private void reset() {
365        if (mPrevEvent != null) {
366            mPrevEvent.recycle();
367            mPrevEvent = null;
368        }
369        if (mCurrEvent != null) {
370            mCurrEvent.recycle();
371            mCurrEvent = null;
372        }
373        mSloppyGesture = false;
374        mGestureInProgress = false;
375    }
376
377    /**
378     * Returns {@code true} if a two-finger scale gesture is in progress.
379     * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
380     */
381    public boolean isInProgress() {
382        return mGestureInProgress;
383    }
384
385    /**
386     * Get the X coordinate of the current gesture's focal point.
387     * If a gesture is in progress, the focal point is directly between
388     * the two pointers forming the gesture.
389     * If a gesture is ending, the focal point is the location of the
390     * remaining pointer on the screen.
391     * If {@link #isInProgress()} would return false, the result of this
392     * function is undefined.
393     *
394     * @return X coordinate of the focal point in pixels.
395     */
396    public float getFocusX() {
397        return mFocusX;
398    }
399
400    /**
401     * Get the Y coordinate of the current gesture's focal point.
402     * If a gesture is in progress, the focal point is directly between
403     * the two pointers forming the gesture.
404     * If a gesture is ending, the focal point is the location of the
405     * remaining pointer on the screen.
406     * If {@link #isInProgress()} would return false, the result of this
407     * function is undefined.
408     *
409     * @return Y coordinate of the focal point in pixels.
410     */
411    public float getFocusY() {
412        return mFocusY;
413    }
414
415    /**
416     * Return the current distance between the two pointers forming the
417     * gesture in progress.
418     *
419     * @return Distance between pointers in pixels.
420     */
421    public float getCurrentSpan() {
422        if (mCurrLen == -1) {
423            final float cvx = mCurrFingerDiffX;
424            final float cvy = mCurrFingerDiffY;
425            mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
426        }
427        return mCurrLen;
428    }
429
430    /**
431     * Return the previous distance between the two pointers forming the
432     * gesture in progress.
433     *
434     * @return Previous distance between pointers in pixels.
435     */
436    public float getPreviousSpan() {
437        if (mPrevLen == -1) {
438            final float pvx = mPrevFingerDiffX;
439            final float pvy = mPrevFingerDiffY;
440            mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
441        }
442        return mPrevLen;
443    }
444
445    /**
446     * Return the scaling factor from the previous scale event to the current
447     * event. This value is defined as
448     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
449     *
450     * @return The current scaling factor.
451     */
452    public float getScaleFactor() {
453        if (mScaleFactor == -1) {
454            mScaleFactor = getCurrentSpan() / getPreviousSpan();
455        }
456        return mScaleFactor;
457    }
458
459    /**
460     * Return the time difference in milliseconds between the previous
461     * accepted scaling event and the current scaling event.
462     *
463     * @return Time difference since the last scaling event in milliseconds.
464     */
465    public long getTimeDelta() {
466        return mTimeDelta;
467    }
468
469    /**
470     * Return the event time of the current event being processed.
471     *
472     * @return Current event time in milliseconds.
473     */
474    public long getEventTime() {
475        return mCurrEvent.getEventTime();
476    }
477}
478