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