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;
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 */
40public 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         */
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         */
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         */
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
112        public boolean onScale(ScaleGestureDetector detector) {
113            return false;
114        }
115
116        public boolean onScaleBegin(ScaleGestureDetector detector) {
117            return true;
118        }
119
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 boolean mQuickScaleEnabled;
132    private boolean mStylusScaleEnabled;
133
134    private float mCurrSpan;
135    private float mPrevSpan;
136    private float mInitialSpan;
137    private float mCurrSpanX;
138    private float mCurrSpanY;
139    private float mPrevSpanX;
140    private float mPrevSpanY;
141    private long mCurrTime;
142    private long mPrevTime;
143    private boolean mInProgress;
144    private int mSpanSlop;
145    private int mMinSpan;
146
147    private final Handler mHandler;
148
149    private float mAnchoredScaleStartX;
150    private float mAnchoredScaleStartY;
151    private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
152
153    private static final long TOUCH_STABILIZE_TIME = 128; // ms
154    private static final float SCALE_FACTOR = .5f;
155    private static final int ANCHORED_SCALE_MODE_NONE = 0;
156    private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1;
157    private static final int ANCHORED_SCALE_MODE_STYLUS = 2;
158
159
160    /**
161     * Consistency verifier for debugging purposes.
162     */
163    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
164            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
165                    new InputEventConsistencyVerifier(this, 0) : null;
166    private GestureDetector mGestureDetector;
167
168    private boolean mEventBeforeOrAboveStartingGestureEvent;
169
170    /**
171     * Creates a ScaleGestureDetector with the supplied listener.
172     * You may only use this constructor from a {@link android.os.Looper Looper} thread.
173     *
174     * @param context the application's context
175     * @param listener the listener invoked for all the callbacks, this must
176     * not be null.
177     *
178     * @throws NullPointerException if {@code listener} is null.
179     */
180    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
181        this(context, listener, null);
182    }
183
184    /**
185     * Creates a ScaleGestureDetector with the supplied listener.
186     * @see android.os.Handler#Handler()
187     *
188     * @param context the application's context
189     * @param listener the listener invoked for all the callbacks, this must
190     * not be null.
191     * @param handler the handler to use for running deferred listener events.
192     *
193     * @throws NullPointerException if {@code listener} is null.
194     */
195    public ScaleGestureDetector(Context context, OnScaleGestureListener listener,
196                                Handler handler) {
197        mContext = context;
198        mListener = listener;
199        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
200
201        final Resources res = context.getResources();
202        mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
203        mHandler = handler;
204        // Quick scale is enabled by default after JB_MR2
205        final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
206        if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
207            setQuickScaleEnabled(true);
208        }
209        // Stylus scale is enabled by default after LOLLIPOP_MR1
210        if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
211            setStylusScaleEnabled(true);
212        }
213    }
214
215    /**
216     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
217     * when appropriate.
218     *
219     * <p>Applications should pass a complete and consistent event stream to this method.
220     * A complete and consistent event stream involves all MotionEvents from the initial
221     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
222     *
223     * @param event The event to process
224     * @return true if the event was processed and the detector wants to receive the
225     *         rest of the MotionEvents in this event stream.
226     */
227    public boolean onTouchEvent(MotionEvent event) {
228        if (mInputEventConsistencyVerifier != null) {
229            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
230        }
231
232        mCurrTime = event.getEventTime();
233
234        final int action = event.getActionMasked();
235
236        // Forward the event to check for double tap gesture
237        if (mQuickScaleEnabled) {
238            mGestureDetector.onTouchEvent(event);
239        }
240
241        final int count = event.getPointerCount();
242        final boolean isStylusButtonDown =
243                (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0;
244
245        final boolean anchoredScaleCancelled =
246                mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown;
247        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
248                action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled;
249
250        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
251            // Reset any scale in progress with the listener.
252            // If it's an ACTION_DOWN we're beginning a new event stream.
253            // This means the app probably didn't give us all the events. Shame on it.
254            if (mInProgress) {
255                mListener.onScaleEnd(this);
256                mInProgress = false;
257                mInitialSpan = 0;
258                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
259            } else if (inAnchoredScaleMode() && streamComplete) {
260                mInProgress = false;
261                mInitialSpan = 0;
262                mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE;
263            }
264
265            if (streamComplete) {
266                return true;
267            }
268        }
269
270        if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode()
271                && !streamComplete && isStylusButtonDown) {
272            // Start of a button scale gesture
273            mAnchoredScaleStartX = event.getX();
274            mAnchoredScaleStartY = event.getY();
275            mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS;
276            mInitialSpan = 0;
277        }
278
279        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
280                action == MotionEvent.ACTION_POINTER_UP ||
281                action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled;
282
283        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
284        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
285
286        // Determine focal point
287        float sumX = 0, sumY = 0;
288        final int div = pointerUp ? count - 1 : count;
289        final float focusX;
290        final float focusY;
291        if (inAnchoredScaleMode()) {
292            // In anchored scale mode, the focal pt is always where the double tap
293            // or button down gesture started
294            focusX = mAnchoredScaleStartX;
295            focusY = mAnchoredScaleStartY;
296            if (event.getY() < focusY) {
297                mEventBeforeOrAboveStartingGestureEvent = true;
298            } else {
299                mEventBeforeOrAboveStartingGestureEvent = false;
300            }
301        } else {
302            for (int i = 0; i < count; i++) {
303                if (skipIndex == i) continue;
304                sumX += event.getX(i);
305                sumY += event.getY(i);
306            }
307
308            focusX = sumX / div;
309            focusY = sumY / div;
310        }
311
312        // Determine average deviation from focal point
313        float devSumX = 0, devSumY = 0;
314        for (int i = 0; i < count; i++) {
315            if (skipIndex == i) continue;
316
317            // Convert the resulting diameter into a radius.
318            devSumX += Math.abs(event.getX(i) - focusX);
319            devSumY += Math.abs(event.getY(i) - focusY);
320        }
321        final float devX = devSumX / div;
322        final float devY = devSumY / div;
323
324        // Span is the average distance between touch points through the focal point;
325        // i.e. the diameter of the circle with a radius of the average deviation from
326        // the focal point.
327        final float spanX = devX * 2;
328        final float spanY = devY * 2;
329        final float span;
330        if (inAnchoredScaleMode()) {
331            span = spanY;
332        } else {
333            span = (float) Math.hypot(spanX, spanY);
334        }
335
336        // Dispatch begin/end events as needed.
337        // If the configuration changes, notify the app to reset its current state by beginning
338        // a fresh scale event stream.
339        final boolean wasInProgress = mInProgress;
340        mFocusX = focusX;
341        mFocusY = focusY;
342        if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) {
343            mListener.onScaleEnd(this);
344            mInProgress = false;
345            mInitialSpan = span;
346        }
347        if (configChanged) {
348            mPrevSpanX = mCurrSpanX = spanX;
349            mPrevSpanY = mCurrSpanY = spanY;
350            mInitialSpan = mPrevSpan = mCurrSpan = span;
351        }
352
353        final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan;
354        if (!mInProgress && span >=  minSpan &&
355                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
356            mPrevSpanX = mCurrSpanX = spanX;
357            mPrevSpanY = mCurrSpanY = spanY;
358            mPrevSpan = mCurrSpan = span;
359            mPrevTime = mCurrTime;
360            mInProgress = mListener.onScaleBegin(this);
361        }
362
363        // Handle motion; focal point and span/scale factor are changing.
364        if (action == MotionEvent.ACTION_MOVE) {
365            mCurrSpanX = spanX;
366            mCurrSpanY = spanY;
367            mCurrSpan = span;
368
369            boolean updatePrev = true;
370
371            if (mInProgress) {
372                updatePrev = mListener.onScale(this);
373            }
374
375            if (updatePrev) {
376                mPrevSpanX = mCurrSpanX;
377                mPrevSpanY = mCurrSpanY;
378                mPrevSpan = mCurrSpan;
379                mPrevTime = mCurrTime;
380            }
381        }
382
383        return true;
384    }
385
386    private boolean inAnchoredScaleMode() {
387        return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE;
388    }
389
390    /**
391     * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks
392     * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default
393     * if the app targets API 19 and newer.
394     * @param scales true to enable quick scaling, false to disable
395     */
396    public void setQuickScaleEnabled(boolean scales) {
397        mQuickScaleEnabled = scales;
398        if (mQuickScaleEnabled && mGestureDetector == null) {
399            GestureDetector.SimpleOnGestureListener gestureListener =
400                    new GestureDetector.SimpleOnGestureListener() {
401                        @Override
402                        public boolean onDoubleTap(MotionEvent e) {
403                            // Double tap: start watching for a swipe
404                            mAnchoredScaleStartX = e.getX();
405                            mAnchoredScaleStartY = e.getY();
406                            mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP;
407                            return true;
408                        }
409                    };
410            mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
411        }
412    }
413
414  /**
415   * Return whether the quick scale gesture, in which the user performs a double tap followed by a
416   * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.
417   */
418    public boolean isQuickScaleEnabled() {
419        return mQuickScaleEnabled;
420    }
421
422    /**
423     * Sets whether the associates {@link OnScaleGestureListener} should receive
424     * onScale callbacks when the user uses a stylus and presses the button.
425     * Note that this is enabled by default if the app targets API 23 and newer.
426     *
427     * @param scales true to enable stylus scaling, false to disable.
428     */
429    public void setStylusScaleEnabled(boolean scales) {
430        mStylusScaleEnabled = scales;
431    }
432
433    /**
434     * Return whether the stylus scale gesture, in which the user uses a stylus and presses the
435     * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)}
436     */
437    public boolean isStylusScaleEnabled() {
438        return mStylusScaleEnabled;
439    }
440
441    /**
442     * Returns {@code true} if a scale gesture is in progress.
443     */
444    public boolean isInProgress() {
445        return mInProgress;
446    }
447
448    /**
449     * Get the X coordinate of the current gesture's focal point.
450     * If a gesture is in progress, the focal point is between
451     * each of the pointers forming the gesture.
452     *
453     * If {@link #isInProgress()} would return false, the result of this
454     * function is undefined.
455     *
456     * @return X coordinate of the focal point in pixels.
457     */
458    public float getFocusX() {
459        return mFocusX;
460    }
461
462    /**
463     * Get the Y coordinate of the current gesture's focal point.
464     * If a gesture is in progress, the focal point is between
465     * each of the pointers forming the gesture.
466     *
467     * If {@link #isInProgress()} would return false, the result of this
468     * function is undefined.
469     *
470     * @return Y coordinate of the focal point in pixels.
471     */
472    public float getFocusY() {
473        return mFocusY;
474    }
475
476    /**
477     * Return the average distance between each of the pointers forming the
478     * gesture in progress through the focal point.
479     *
480     * @return Distance between pointers in pixels.
481     */
482    public float getCurrentSpan() {
483        return mCurrSpan;
484    }
485
486    /**
487     * Return the average X distance between each of the pointers forming the
488     * gesture in progress through the focal point.
489     *
490     * @return Distance between pointers in pixels.
491     */
492    public float getCurrentSpanX() {
493        return mCurrSpanX;
494    }
495
496    /**
497     * Return the average Y distance between each of the pointers forming the
498     * gesture in progress through the focal point.
499     *
500     * @return Distance between pointers in pixels.
501     */
502    public float getCurrentSpanY() {
503        return mCurrSpanY;
504    }
505
506    /**
507     * Return the previous average distance between each of the pointers forming the
508     * gesture in progress through the focal point.
509     *
510     * @return Previous distance between pointers in pixels.
511     */
512    public float getPreviousSpan() {
513        return mPrevSpan;
514    }
515
516    /**
517     * Return the previous average X distance between each of the pointers forming the
518     * gesture in progress through the focal point.
519     *
520     * @return Previous distance between pointers in pixels.
521     */
522    public float getPreviousSpanX() {
523        return mPrevSpanX;
524    }
525
526    /**
527     * Return the previous average Y distance between each of the pointers forming the
528     * gesture in progress through the focal point.
529     *
530     * @return Previous distance between pointers in pixels.
531     */
532    public float getPreviousSpanY() {
533        return mPrevSpanY;
534    }
535
536    /**
537     * Return the scaling factor from the previous scale event to the current
538     * event. This value is defined as
539     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
540     *
541     * @return The current scaling factor.
542     */
543    public float getScaleFactor() {
544        if (inAnchoredScaleMode()) {
545            // Drag is moving up; the further away from the gesture
546            // start, the smaller the span should be, the closer,
547            // the larger the span, and therefore the larger the scale
548            final boolean scaleUp =
549                    (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
550                    (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
551            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
552            return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
553        }
554        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
555    }
556
557    /**
558     * Return the time difference in milliseconds between the previous
559     * accepted scaling event and the current scaling event.
560     *
561     * @return Time difference since the last scaling event in milliseconds.
562     */
563    public long getTimeDelta() {
564        return mCurrTime - mPrevTime;
565    }
566
567    /**
568     * Return the event time of the current event being processed.
569     *
570     * @return Current event time in milliseconds.
571     */
572    public long getEventTime() {
573        return mCurrTime;
574    }
575}