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        final int action = event.getActionMasked();
263
264        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
265                action == MotionEvent.ACTION_CANCEL;
266        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
267            // Reset any scale in progress with the listener.
268            // If it's an ACTION_DOWN we're beginning a new event stream.
269            // This means the app probably didn't give us all the events. Shame on it.
270            if (mInProgress) {
271                mListener.onScaleEnd(this);
272                mInProgress = false;
273                mInitialSpan = 0;
274            }
275
276            if (streamComplete) {
277                clearTouchHistory();
278                return true;
279            }
280        }
281
282        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
283                action == MotionEvent.ACTION_POINTER_UP ||
284                action == MotionEvent.ACTION_POINTER_DOWN;
285        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
286        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
287
288        // Determine focal point
289        float sumX = 0, sumY = 0;
290        final int count = event.getPointerCount();
291        for (int i = 0; i < count; i++) {
292            if (skipIndex == i) continue;
293            sumX += event.getX(i);
294            sumY += event.getY(i);
295        }
296        final int div = pointerUp ? count - 1 : count;
297        final float focusX = sumX / div;
298        final float focusY = sumY / div;
299
300
301        addTouchHistory(event);
302
303        // Determine average deviation from focal point
304        float devSumX = 0, devSumY = 0;
305        for (int i = 0; i < count; i++) {
306            if (skipIndex == i) continue;
307
308            // Convert the resulting diameter into a radius.
309            final float touchSize = mTouchHistoryLastAccepted / 2;
310            devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
311            devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
312        }
313        final float devX = devSumX / div;
314        final float devY = devSumY / div;
315
316        // Span is the average distance between touch points through the focal point;
317        // i.e. the diameter of the circle with a radius of the average deviation from
318        // the focal point.
319        final float spanX = devX * 2;
320        final float spanY = devY * 2;
321        final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
322
323        // Dispatch begin/end events as needed.
324        // If the configuration changes, notify the app to reset its current state by beginning
325        // a fresh scale event stream.
326        final boolean wasInProgress = mInProgress;
327        mFocusX = focusX;
328        mFocusY = focusY;
329        if (mInProgress && (span < mMinSpan || configChanged)) {
330            mListener.onScaleEnd(this);
331            mInProgress = false;
332            mInitialSpan = span;
333        }
334        if (configChanged) {
335            mPrevSpanX = mCurrSpanX = spanX;
336            mPrevSpanY = mCurrSpanY = spanY;
337            mInitialSpan = mPrevSpan = mCurrSpan = span;
338        }
339        if (!mInProgress && span >= mMinSpan &&
340                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
341            mPrevSpanX = mCurrSpanX = spanX;
342            mPrevSpanY = mCurrSpanY = spanY;
343            mPrevSpan = mCurrSpan = span;
344            mInProgress = mListener.onScaleBegin(this);
345        }
346
347        // Handle motion; focal point and span/scale factor are changing.
348        if (action == MotionEvent.ACTION_MOVE) {
349            mCurrSpanX = spanX;
350            mCurrSpanY = spanY;
351            mCurrSpan = span;
352
353            boolean updatePrev = true;
354            if (mInProgress) {
355                updatePrev = mListener.onScale(this);
356            }
357
358            if (updatePrev) {
359                mPrevSpanX = mCurrSpanX;
360                mPrevSpanY = mCurrSpanY;
361                mPrevSpan = mCurrSpan;
362            }
363        }
364
365        return true;
366    }
367
368    /**
369     * Returns {@code true} if a scale gesture is in progress.
370     */
371    public boolean isInProgress() {
372        return mInProgress;
373    }
374
375    /**
376     * Get the X coordinate of the current gesture's focal point.
377     * If a gesture is in progress, the focal point is between
378     * each of the pointers forming the gesture.
379     *
380     * If {@link #isInProgress()} would return false, the result of this
381     * function is undefined.
382     *
383     * @return X coordinate of the focal point in pixels.
384     */
385    public float getFocusX() {
386        return mFocusX;
387    }
388
389    /**
390     * Get the Y coordinate of the current gesture's focal point.
391     * If a gesture is in progress, the focal point is between
392     * each of the pointers forming the gesture.
393     *
394     * If {@link #isInProgress()} would return false, the result of this
395     * function is undefined.
396     *
397     * @return Y coordinate of the focal point in pixels.
398     */
399    public float getFocusY() {
400        return mFocusY;
401    }
402
403    /**
404     * Return the average distance between each of the pointers forming the
405     * gesture in progress through the focal point.
406     *
407     * @return Distance between pointers in pixels.
408     */
409    public float getCurrentSpan() {
410        return mCurrSpan;
411    }
412
413    /**
414     * Return the average X distance between each of the pointers forming the
415     * gesture in progress through the focal point.
416     *
417     * @return Distance between pointers in pixels.
418     */
419    public float getCurrentSpanX() {
420        return mCurrSpanX;
421    }
422
423    /**
424     * Return the average Y distance between each of the pointers forming the
425     * gesture in progress through the focal point.
426     *
427     * @return Distance between pointers in pixels.
428     */
429    public float getCurrentSpanY() {
430        return mCurrSpanY;
431    }
432
433    /**
434     * Return the previous average distance between each of the pointers forming the
435     * gesture in progress through the focal point.
436     *
437     * @return Previous distance between pointers in pixels.
438     */
439    public float getPreviousSpan() {
440        return mPrevSpan;
441    }
442
443    /**
444     * Return the previous average X distance between each of the pointers forming the
445     * gesture in progress through the focal point.
446     *
447     * @return Previous distance between pointers in pixels.
448     */
449    public float getPreviousSpanX() {
450        return mPrevSpanX;
451    }
452
453    /**
454     * Return the previous average Y distance between each of the pointers forming the
455     * gesture in progress through the focal point.
456     *
457     * @return Previous distance between pointers in pixels.
458     */
459    public float getPreviousSpanY() {
460        return mPrevSpanY;
461    }
462
463    /**
464     * Return the scaling factor from the previous scale event to the current
465     * event. This value is defined as
466     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
467     *
468     * @return The current scaling factor.
469     */
470    public float getScaleFactor() {
471        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
472    }
473
474    /**
475     * Return the time difference in milliseconds between the previous
476     * accepted scaling event and the current scaling event.
477     *
478     * @return Time difference since the last scaling event in milliseconds.
479     */
480    public long getTimeDelta() {
481        return mCurrTime - mPrevTime;
482    }
483
484    /**
485     * Return the event time of the current event being processed.
486     *
487     * @return Current event time in milliseconds.
488     */
489    public long getEventTime() {
490        return mCurrTime;
491    }
492}
493