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.SystemClock;
22import android.util.FloatMath;
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 float mCurrSpan;
132    private float mPrevSpan;
133    private float mInitialSpan;
134    private float mCurrSpanX;
135    private float mCurrSpanY;
136    private float mPrevSpanX;
137    private float mPrevSpanY;
138    private long mCurrTime;
139    private long mPrevTime;
140    private boolean mInProgress;
141    private int mSpanSlop;
142    private int mMinSpan;
143
144    // Bounds for recently seen values
145    private float mTouchUpper;
146    private float mTouchLower;
147    private float mTouchHistoryLastAccepted;
148    private int mTouchHistoryDirection;
149    private long mTouchHistoryLastAcceptedTime;
150    private int mTouchMinMajor;
151
152    private static final long TOUCH_STABILIZE_TIME = 128; // ms
153    private static final int TOUCH_MIN_MAJOR = 48; // dp
154
155    /**
156     * Consistency verifier for debugging purposes.
157     */
158    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
159            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
160                    new InputEventConsistencyVerifier(this, 0) : null;
161
162    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
163        mContext = context;
164        mListener = listener;
165        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
166
167        final Resources res = context.getResources();
168        mTouchMinMajor = res.getDimensionPixelSize(
169                com.android.internal.R.dimen.config_minScalingTouchMajor);
170        mMinSpan = res.getDimensionPixelSize(
171                com.android.internal.R.dimen.config_minScalingSpan);
172    }
173
174    /**
175     * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
176     * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
177     * @param ev MotionEvent to add to the ongoing history
178     */
179    private void addTouchHistory(MotionEvent ev) {
180        final long currentTime = SystemClock.uptimeMillis();
181        final int count = ev.getPointerCount();
182        boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
183        float total = 0;
184        int sampleCount = 0;
185        for (int i = 0; i < count; i++) {
186            final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
187            final int historySize = ev.getHistorySize();
188            final int pointerSampleCount = historySize + 1;
189            for (int h = 0; h < pointerSampleCount; h++) {
190                float major;
191                if (h < historySize) {
192                    major = ev.getHistoricalTouchMajor(i, h);
193                } else {
194                    major = ev.getTouchMajor(i);
195                }
196                if (major < mTouchMinMajor) major = mTouchMinMajor;
197                total += major;
198
199                if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
200                    mTouchUpper = major;
201                }
202                if (Float.isNaN(mTouchLower) || major < mTouchLower) {
203                    mTouchLower = major;
204                }
205
206                if (hasLastAccepted) {
207                    final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
208                    if (directionSig != mTouchHistoryDirection ||
209                            (directionSig == 0 && mTouchHistoryDirection == 0)) {
210                        mTouchHistoryDirection = directionSig;
211                        final long time = h < historySize ? ev.getHistoricalEventTime(h)
212                                : ev.getEventTime();
213                        mTouchHistoryLastAcceptedTime = time;
214                        accept = false;
215                    }
216                }
217            }
218            sampleCount += pointerSampleCount;
219        }
220
221        final float avg = total / sampleCount;
222
223        if (accept) {
224            float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
225            mTouchUpper = (mTouchUpper + newAccepted) / 2;
226            mTouchLower = (mTouchLower + newAccepted) / 2;
227            mTouchHistoryLastAccepted = newAccepted;
228            mTouchHistoryDirection = 0;
229            mTouchHistoryLastAcceptedTime = ev.getEventTime();
230        }
231    }
232
233    /**
234     * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
235     * @see #addTouchHistory(MotionEvent)
236     */
237    private void clearTouchHistory() {
238        mTouchUpper = Float.NaN;
239        mTouchLower = Float.NaN;
240        mTouchHistoryLastAccepted = Float.NaN;
241        mTouchHistoryDirection = 0;
242        mTouchHistoryLastAcceptedTime = 0;
243    }
244
245    /**
246     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
247     * when appropriate.
248     *
249     * <p>Applications should pass a complete and consistent event stream to this method.
250     * A complete and consistent event stream involves all MotionEvents from the initial
251     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
252     *
253     * @param event The event to process
254     * @return true if the event was processed and the detector wants to receive the
255     *         rest of the MotionEvents in this event stream.
256     */
257    public boolean onTouchEvent(MotionEvent event) {
258        if (mInputEventConsistencyVerifier != null) {
259            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
260        }
261
262        mCurrTime = event.getEventTime();
263
264        final int action = event.getActionMasked();
265
266        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
267                action == MotionEvent.ACTION_CANCEL;
268        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
269            // Reset any scale in progress with the listener.
270            // If it's an ACTION_DOWN we're beginning a new event stream.
271            // This means the app probably didn't give us all the events. Shame on it.
272            if (mInProgress) {
273                mListener.onScaleEnd(this);
274                mInProgress = false;
275                mInitialSpan = 0;
276            }
277
278            if (streamComplete) {
279                clearTouchHistory();
280                return true;
281            }
282        }
283
284        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
285                action == MotionEvent.ACTION_POINTER_UP ||
286                action == MotionEvent.ACTION_POINTER_DOWN;
287        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
288        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
289
290        // Determine focal point
291        float sumX = 0, sumY = 0;
292        final int count = event.getPointerCount();
293        for (int i = 0; i < count; i++) {
294            if (skipIndex == i) continue;
295            sumX += event.getX(i);
296            sumY += event.getY(i);
297        }
298        final int div = pointerUp ? count - 1 : count;
299        final float focusX = sumX / div;
300        final float focusY = sumY / div;
301
302
303        addTouchHistory(event);
304
305        // Determine average deviation from focal point
306        float devSumX = 0, devSumY = 0;
307        for (int i = 0; i < count; i++) {
308            if (skipIndex == i) continue;
309
310            // Convert the resulting diameter into a radius.
311            final float touchSize = mTouchHistoryLastAccepted / 2;
312            devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
313            devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
314        }
315        final float devX = devSumX / div;
316        final float devY = devSumY / div;
317
318        // Span is the average distance between touch points through the focal point;
319        // i.e. the diameter of the circle with a radius of the average deviation from
320        // the focal point.
321        final float spanX = devX * 2;
322        final float spanY = devY * 2;
323        final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
324
325        // Dispatch begin/end events as needed.
326        // If the configuration changes, notify the app to reset its current state by beginning
327        // a fresh scale event stream.
328        final boolean wasInProgress = mInProgress;
329        mFocusX = focusX;
330        mFocusY = focusY;
331        if (mInProgress && (span < mMinSpan || configChanged)) {
332            mListener.onScaleEnd(this);
333            mInProgress = false;
334            mInitialSpan = span;
335        }
336        if (configChanged) {
337            mPrevSpanX = mCurrSpanX = spanX;
338            mPrevSpanY = mCurrSpanY = spanY;
339            mInitialSpan = mPrevSpan = mCurrSpan = span;
340        }
341        if (!mInProgress && span >= mMinSpan &&
342                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
343            mPrevSpanX = mCurrSpanX = spanX;
344            mPrevSpanY = mCurrSpanY = spanY;
345            mPrevSpan = mCurrSpan = span;
346            mPrevTime = mCurrTime;
347            mInProgress = mListener.onScaleBegin(this);
348        }
349
350        // Handle motion; focal point and span/scale factor are changing.
351        if (action == MotionEvent.ACTION_MOVE) {
352            mCurrSpanX = spanX;
353            mCurrSpanY = spanY;
354            mCurrSpan = span;
355
356            boolean updatePrev = true;
357            if (mInProgress) {
358                updatePrev = mListener.onScale(this);
359            }
360
361            if (updatePrev) {
362                mPrevSpanX = mCurrSpanX;
363                mPrevSpanY = mCurrSpanY;
364                mPrevSpan = mCurrSpan;
365                mPrevTime = mCurrTime;
366            }
367        }
368
369        return true;
370    }
371
372    /**
373     * Returns {@code true} if a scale gesture is in progress.
374     */
375    public boolean isInProgress() {
376        return mInProgress;
377    }
378
379    /**
380     * Get the X coordinate of the current gesture's focal point.
381     * If a gesture is in progress, the focal point is between
382     * each of the pointers forming the gesture.
383     *
384     * If {@link #isInProgress()} would return false, the result of this
385     * function is undefined.
386     *
387     * @return X coordinate of the focal point in pixels.
388     */
389    public float getFocusX() {
390        return mFocusX;
391    }
392
393    /**
394     * Get the Y coordinate of the current gesture's focal point.
395     * If a gesture is in progress, the focal point is between
396     * each of the pointers forming the gesture.
397     *
398     * If {@link #isInProgress()} would return false, the result of this
399     * function is undefined.
400     *
401     * @return Y coordinate of the focal point in pixels.
402     */
403    public float getFocusY() {
404        return mFocusY;
405    }
406
407    /**
408     * Return the average distance between each of the pointers forming the
409     * gesture in progress through the focal point.
410     *
411     * @return Distance between pointers in pixels.
412     */
413    public float getCurrentSpan() {
414        return mCurrSpan;
415    }
416
417    /**
418     * Return the average X distance between each of the pointers forming the
419     * gesture in progress through the focal point.
420     *
421     * @return Distance between pointers in pixels.
422     */
423    public float getCurrentSpanX() {
424        return mCurrSpanX;
425    }
426
427    /**
428     * Return the average Y distance between each of the pointers forming the
429     * gesture in progress through the focal point.
430     *
431     * @return Distance between pointers in pixels.
432     */
433    public float getCurrentSpanY() {
434        return mCurrSpanY;
435    }
436
437    /**
438     * Return the previous average distance between each of the pointers forming the
439     * gesture in progress through the focal point.
440     *
441     * @return Previous distance between pointers in pixels.
442     */
443    public float getPreviousSpan() {
444        return mPrevSpan;
445    }
446
447    /**
448     * Return the previous average X distance between each of the pointers forming the
449     * gesture in progress through the focal point.
450     *
451     * @return Previous distance between pointers in pixels.
452     */
453    public float getPreviousSpanX() {
454        return mPrevSpanX;
455    }
456
457    /**
458     * Return the previous average Y distance between each of the pointers forming the
459     * gesture in progress through the focal point.
460     *
461     * @return Previous distance between pointers in pixels.
462     */
463    public float getPreviousSpanY() {
464        return mPrevSpanY;
465    }
466
467    /**
468     * Return the scaling factor from the previous scale event to the current
469     * event. This value is defined as
470     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
471     *
472     * @return The current scaling factor.
473     */
474    public float getScaleFactor() {
475        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
476    }
477
478    /**
479     * Return the time difference in milliseconds between the previous
480     * accepted scaling event and the current scaling event.
481     *
482     * @return Time difference since the last scaling event in milliseconds.
483     */
484    public long getTimeDelta() {
485        return mCurrTime - mPrevTime;
486    }
487
488    /**
489     * Return the event time of the current event being processed.
490     *
491     * @return Current event time in milliseconds.
492     */
493    public long getEventTime() {
494        return mCurrTime;
495    }
496}
497