ScaleGestureDetector.java revision c6df18f5fa9f08183af96de7433fd80d74325318
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.content.res.Resources;
21import android.os.Build;
22import android.os.Handler;
23import android.os.SystemClock;
24
25/**
26 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
27 * The {@link OnScaleGestureListener} callback will notify users when a particular
28 * gesture event has occurred.
29 *
30 * This class should only be used with {@link MotionEvent}s reported via touch.
31 *
32 * To use this class:
33 * <ul>
34 *  <li>Create an instance of the {@code ScaleGestureDetector} for your
35 *      {@link View}
36 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
37 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
38 *          callback will be executed when the events occur.
39 * </ul>
40 */
41public class ScaleGestureDetector {
42    private static final String TAG = "ScaleGestureDetector";
43
44    /**
45     * The listener for receiving notifications when gestures occur.
46     * If you want to listen for all the different gestures then implement
47     * this interface. If you only want to listen for a subset it might
48     * be easier to extend {@link SimpleOnScaleGestureListener}.
49     *
50     * An application will receive events in the following order:
51     * <ul>
52     *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
53     *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
54     *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
55     * </ul>
56     */
57    public interface OnScaleGestureListener {
58        /**
59         * Responds to scaling events for a gesture in progress.
60         * Reported by pointer motion.
61         *
62         * @param detector The detector reporting the event - use this to
63         *          retrieve extended info about event state.
64         * @return Whether or not the detector should consider this event
65         *          as handled. If an event was not handled, the detector
66         *          will continue to accumulate movement until an event is
67         *          handled. This can be useful if an application, for example,
68         *          only wants to update scaling factors if the change is
69         *          greater than 0.01.
70         */
71        public boolean onScale(ScaleGestureDetector detector);
72
73        /**
74         * Responds to the beginning of a scaling gesture. Reported by
75         * new pointers going down.
76         *
77         * @param detector The detector reporting the event - use this to
78         *          retrieve extended info about event state.
79         * @return Whether or not the detector should continue recognizing
80         *          this gesture. For example, if a gesture is beginning
81         *          with a focal point outside of a region where it makes
82         *          sense, onScaleBegin() may return false to ignore the
83         *          rest of the gesture.
84         */
85        public boolean onScaleBegin(ScaleGestureDetector detector);
86
87        /**
88         * Responds to the end of a scale gesture. Reported by existing
89         * pointers going up.
90         *
91         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
92         * and {@link ScaleGestureDetector#getFocusY()} will return focal point
93         * of the pointers remaining on the screen.
94         *
95         * @param detector The detector reporting the event - use this to
96         *          retrieve extended info about event state.
97         */
98        public void onScaleEnd(ScaleGestureDetector detector);
99    }
100
101    /**
102     * A convenience class to extend when you only want to listen for a subset
103     * of scaling-related events. This implements all methods in
104     * {@link OnScaleGestureListener} but does nothing.
105     * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
106     * {@code false} so that a subclass can retrieve the accumulated scale
107     * factor in an overridden onScaleEnd.
108     * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
109     * {@code true}.
110     */
111    public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
112
113        public boolean onScale(ScaleGestureDetector detector) {
114            return false;
115        }
116
117        public boolean onScaleBegin(ScaleGestureDetector detector) {
118            return true;
119        }
120
121        public void onScaleEnd(ScaleGestureDetector detector) {
122            // Intentionally empty
123        }
124    }
125
126    private final Context mContext;
127    private final OnScaleGestureListener mListener;
128
129    private float mFocusX;
130    private float mFocusY;
131
132    private boolean mQuickScaleEnabled;
133    private boolean mStylusScaleEnabled;
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    private final Handler mHandler;
149
150    private float mAnchoredScaleStartX;
151    private float mAnchoredScaleStartY;
152    private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
153
154    private static final long TOUCH_STABILIZE_TIME = 128; // ms
155    private static final float SCALE_FACTOR = .5f;
156    private static final int ANCHORED_SCALE_MODE_NONE = 0;
157    private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
158    private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
159
160
161    /**
162     * Consistency verifier for debugging purposes.
163     */
164    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
165            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
166                    new InputEventConsistencyVerifier(this, 0) : null;
167    private GestureDetector mGestureDetector;
168
169    private boolean mEventBeforeOrAboveStartingGestureEvent;
170
171    /**
172     * Creates a ScaleGestureDetector with the supplied listener.
173     * You may only use this constructor from a {@link android.os.Looper Looper} thread.
174     *
175     * @param context the application's context
176     * @param listener the listener invoked for all the callbacks, this must
177     * not be null.
178     *
179     * @throws NullPointerException if {@code listener} is null.
180     */
181    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
182        this(context, listener, null);
183    }
184
185    /**
186     * Creates a ScaleGestureDetector with the supplied listener.
187     * @see android.os.Handler#Handler()
188     *
189     * @param context the application's context
190     * @param listener the listener invoked for all the callbacks, this must
191     * not be null.
192     * @param handler the handler to use for running deferred listener events.
193     *
194     * @throws NullPointerException if {@code listener} is null.
195     */
196    public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
197                                Handler handler) {
198        mContext = context;
199        mListener = listener;
200        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
201
202        final Resources res = context.getResources();
203        mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
204        mHandler = handler;
205        // Quick scale is enabled by default after JB_MR2
206        final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
207        if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
208            setQuickScaleEnabled(true);
209        }
210        // Stylus scale is enabled by default after LOLLIPOP_MR1
211        if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
212            setStylusScaleEnabled(true);
213        }
214    }
215
216    /**
217     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
218     * when appropriate.
219     *
220     * <p>Applications should pass a complete and consistent event stream to this method.
221     * A complete and consistent event stream involves all MotionEvents from the initial
222     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
223     *
224     * @param event The event to process
225     * @return true if the event was processed and the detector wants to receive the
226     *         rest of the MotionEvents in this event stream.
227     */
228    public boolean onTouchEvent(MotionEvent event) {
229        if (mInputEventConsistencyVerifier != null) {
230            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
231        }
232
233        mCurrTime = event.getEventTime();
234
235        final int action = event.getActionMasked();
236
237        // Forward the event to check for double tap gesture
238        if (mQuickScaleEnabled) {
239            mGestureDetector.onTouchEvent(event);
240        }
241
242        final int count = event.getPointerCount();
243        final boolean isStylusButtonDown =
244                (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
245
246        final boolean anchoredScaleCancelled =
247                mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
248        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
249                action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
250
251        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
252            // Reset any scale in progress with the listener.
253            // If it's an ACTION_DOWN we're beginning a new event stream.
254            // This means the app probably didn't give us all the events. Shame on it.
255            if (mInProgress) {
256                mListener.onScaleEnd(this);
257                mInProgress = false;
258                mInitialSpan = 0;
259                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
260            } else if (inAnchoredScaleMode() && streamComplete) {
261                mInProgress = false;
262                mInitialSpan = 0;
263                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
264            }
265
266            if (streamComplete) {
267                return true;
268            }
269        }
270
271        if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
272                && !streamComplete && isStylusButtonDown) {
273            // Start of a button scale gesture
274            mAnchoredScaleStartX = event.getX();
275            mAnchoredScaleStartY = event.getY();
276            mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
277            mInitialSpan = 0;
278        }
279
280        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
281                action == MotionEvent.ACTION_POINTER_UP ||
282                action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
283
284        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
285        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
286
287        // Determine focal point
288        float sumX = 0, sumY = 0;
289        final int div = pointerUp ? count - 1 : count;
290        final float focusX;
291        final float focusY;
292        if (inAnchoredScaleMode()) {
293            // In anchored scale mode, the focal pt is always where the double tap
294            // or button down gesture started
295            focusX = mAnchoredScaleStartX;
296            focusY = mAnchoredScaleStartY;
297            if (event.getY() < focusY) {
298                mEventBeforeOrAboveStartingGestureEvent = true;
299            } else {
300                mEventBeforeOrAboveStartingGestureEvent = false;
301            }
302        } else {
303            for (int i = 0; i < count; i++) {
304                if (skipIndex == i) continue;
305                sumX += event.getX(i);
306                sumY += event.getY(i);
307            }
308
309            focusX = sumX / div;
310            focusY = sumY / div;
311        }
312
313        // Determine average deviation from focal point
314        float devSumX = 0, devSumY = 0;
315        for (int i = 0; i < count; i++) {
316            if (skipIndex == i) continue;
317
318            // Convert the resulting diameter into a radius.
319            devSumX += Math.abs(event.getX(i) - focusX);
320            devSumY += Math.abs(event.getY(i) - focusY);
321        }
322        final float devX = devSumX / div;
323        final float devY = devSumY / div;
324
325        // Span is the average distance between touch points through the focal point;
326        // i.e. the diameter of the circle with a radius of the average deviation from
327        // the focal point.
328        final float spanX = devX * 2;
329        final float spanY = devY * 2;
330        final float span;
331        if (inAnchoredScaleMode()) {
332            span = spanY;
333        } else {
334            span = (float) Math.hypot(spanX, spanY);
335        }
336
337        // Dispatch begin/end events as needed.
338        // If the configuration changes, notify the app to reset its current state by beginning
339        // a fresh scale event stream.
340        final boolean wasInProgress = mInProgress;
341        mFocusX = focusX;
342        mFocusY = focusY;
343        if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
344            mListener.onScaleEnd(this);
345            mInProgress = false;
346            mInitialSpan = span;
347        }
348        if (configChanged) {
349            mPrevSpanX = mCurrSpanX = spanX;
350            mPrevSpanY = mCurrSpanY = spanY;
351            mInitialSpan = mPrevSpan = mCurrSpan = span;
352        }
353
354        final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
355        if (!mInProgress && span >=  minSpan &&
356                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
357            mPrevSpanX = mCurrSpanX = spanX;
358            mPrevSpanY = mCurrSpanY = spanY;
359            mPrevSpan = mCurrSpan = span;
360            mPrevTime = mCurrTime;
361            mInProgress = mListener.onScaleBegin(this);
362        }
363
364        // Handle motion; focal point and span/scale factor are changing.
365        if (action == MotionEvent.ACTION_MOVE) {
366            mCurrSpanX = spanX;
367            mCurrSpanY = spanY;
368            mCurrSpan = span;
369
370            boolean updatePrev = true;
371
372            if (mInProgress) {
373                updatePrev = mListener.onScale(this);
374            }
375
376            if (updatePrev) {
377                mPrevSpanX = mCurrSpanX;
378                mPrevSpanY = mCurrSpanY;
379                mPrevSpan = mCurrSpan;
380                mPrevTime = mCurrTime;
381            }
382        }
383
384        return true;
385    }
386
387    private boolean inAnchoredScaleMode() {
388        return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
389    }
390
391    /**
392     * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
393     * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
394     * if the app targets API 19 and newer.
395     * @param scales true to enable quick scaling, false to disable
396     */
397    public void setQuickScaleEnabled(boolean scales) {
398        mQuickScaleEnabled = scales;
399        if (mQuickScaleEnabled && mGestureDetector == null) {
400            GestureDetector.SimpleOnGestureListener gestureListener =
401                    new GestureDetector.SimpleOnGestureListener() {
402                        @Override
403                        public boolean onDoubleTap(MotionEvent e) {
404                            // Double tap: start watching for a swipe
405                            mAnchoredScaleStartX = e.getX();
406                            mAnchoredScaleStartY = e.getY();
407                            mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
408                            return true;
409                        }
410                    };
411            mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
412        }
413    }
414
415  /**
416   * Return whether the quick scale gesture, in which the user performs a double tap followed by a
417   * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
418   */
419    public boolean isQuickScaleEnabled() {
420        return mQuickScaleEnabled;
421    }
422
423    /**
424     * Sets whether the associates {@link OnScaleGestureListener} should receive
425     * onScale callbacks when the user uses a stylus and presses the button.
426     * Note that this is enabled by default if the app targets API 23 and newer.
427     *
428     * @param scales true to enable stylus scaling, false to disable.
429     */
430    public void setStylusScaleEnabled(boolean scales) {
431        mStylusScaleEnabled = scales;
432    }
433
434    /**
435     * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
436     * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
437     */
438    public boolean isStylusScaleEnabled() {
439        return mStylusScaleEnabled;
440    }
441
442    /**
443     * Returns {@code true} if a scale gesture is in progress.
444     */
445    public boolean isInProgress() {
446        return mInProgress;
447    }
448
449    /**
450     * Get the X coordinate of the current gesture's focal point.
451     * If a gesture is in progress, the focal point is between
452     * each of the pointers forming the gesture.
453     *
454     * If {@link #isInProgress()} would return false, the result of this
455     * function is undefined.
456     *
457     * @return X coordinate of the focal point in pixels.
458     */
459    public float getFocusX() {
460        return mFocusX;
461    }
462
463    /**
464     * Get the Y coordinate of the current gesture's focal point.
465     * If a gesture is in progress, the focal point is between
466     * each of the pointers forming the gesture.
467     *
468     * If {@link #isInProgress()} would return false, the result of this
469     * function is undefined.
470     *
471     * @return Y coordinate of the focal point in pixels.
472     */
473    public float getFocusY() {
474        return mFocusY;
475    }
476
477    /**
478     * Return the average distance between each of the pointers forming the
479     * gesture in progress through the focal point.
480     *
481     * @return Distance between pointers in pixels.
482     */
483    public float getCurrentSpan() {
484        return mCurrSpan;
485    }
486
487    /**
488     * Return the average X distance between each of the pointers forming the
489     * gesture in progress through the focal point.
490     *
491     * @return Distance between pointers in pixels.
492     */
493    public float getCurrentSpanX() {
494        return mCurrSpanX;
495    }
496
497    /**
498     * Return the average Y distance between each of the pointers forming the
499     * gesture in progress through the focal point.
500     *
501     * @return Distance between pointers in pixels.
502     */
503    public float getCurrentSpanY() {
504        return mCurrSpanY;
505    }
506
507    /**
508     * Return the previous average distance between each of the pointers forming the
509     * gesture in progress through the focal point.
510     *
511     * @return Previous distance between pointers in pixels.
512     */
513    public float getPreviousSpan() {
514        return mPrevSpan;
515    }
516
517    /**
518     * Return the previous average X distance between each of the pointers forming the
519     * gesture in progress through the focal point.
520     *
521     * @return Previous distance between pointers in pixels.
522     */
523    public float getPreviousSpanX() {
524        return mPrevSpanX;
525    }
526
527    /**
528     * Return the previous average Y distance between each of the pointers forming the
529     * gesture in progress through the focal point.
530     *
531     * @return Previous distance between pointers in pixels.
532     */
533    public float getPreviousSpanY() {
534        return mPrevSpanY;
535    }
536
537    /**
538     * Return the scaling factor from the previous scale event to the current
539     * event. This value is defined as
540     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
541     *
542     * @return The current scaling factor.
543     */
544    public float getScaleFactor() {
545        if (inAnchoredScaleMode()) {
546            // Drag is moving up; the further away from the gesture
547            // start, the smaller the span should be, the closer,
548            // the larger the span, and therefore the larger the scale
549            final boolean scaleUp =
550                    (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
551                    (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
552            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
553            return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
554        }
555        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
556    }
557
558    /**
559     * Return the time difference in milliseconds between the previous
560     * accepted scaling event and the current scaling event.
561     *
562     * @return Time difference since the last scaling event in milliseconds.
563     */
564    public long getTimeDelta() {
565        return mCurrTime - mPrevTime;
566    }
567
568    /**
569     * Return the event time of the current event being processed.
570     *
571     * @return Current event time in milliseconds.
572     */
573    public long getEventTime() {
574        return mCurrTime;
575    }
576}