ScaleGestureDetector.java revision 5b5c414e31c4a8433a3290b931687a05dadc97b6
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.os.SystemClock;
21import android.util.FloatMath;
22
23/**
24 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
25 * The {@link OnScaleGestureListener} callback will notify users when a particular
26 * gesture event has occurred.
27 *
28 * This class should only be used with {@link MotionEvent}s reported via touch.
29 *
30 * To use this class:
31 * <ul>
32 *  <li>Create an instance of the {@code ScaleGestureDetector} for your
33 *      {@link View}
34 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
35 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
36 *          callback will be executed when the events occur.
37 * </ul>
38 */
39public class ScaleGestureDetector {
40    private static final String TAG = "ScaleGestureDetector";
41
42    /**
43     * The listener for receiving notifications when gestures occur.
44     * If you want to listen for all the different gestures then implement
45     * this interface. If you only want to listen for a subset it might
46     * be easier to extend {@link SimpleOnScaleGestureListener}.
47     *
48     * An application will receive events in the following order:
49     * <ul>
50     *  <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
51     *  <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
52     *  <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
53     * </ul>
54     */
55    public interface OnScaleGestureListener {
56        /**
57         * Responds to scaling events for a gesture in progress.
58         * Reported by pointer motion.
59         *
60         * @param detector The detector reporting the event - use this to
61         *          retrieve extended info about event state.
62         * @return Whether or not the detector should consider this event
63         *          as handled. If an event was not handled, the detector
64         *          will continue to accumulate movement until an event is
65         *          handled. This can be useful if an application, for example,
66         *          only wants to update scaling factors if the change is
67         *          greater than 0.01.
68         */
69        public boolean onScale(ScaleGestureDetector detector);
70
71        /**
72         * Responds to the beginning of a scaling gesture. Reported by
73         * new pointers going down.
74         *
75         * @param detector The detector reporting the event - use this to
76         *          retrieve extended info about event state.
77         * @return Whether or not the detector should continue recognizing
78         *          this gesture. For example, if a gesture is beginning
79         *          with a focal point outside of a region where it makes
80         *          sense, onScaleBegin() may return false to ignore the
81         *          rest of the gesture.
82         */
83        public boolean onScaleBegin(ScaleGestureDetector detector);
84
85        /**
86         * Responds to the end of a scale gesture. Reported by existing
87         * pointers going up.
88         *
89         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
90         * and {@link ScaleGestureDetector#getFocusY()} will return focal point
91         * of the pointers remaining on the screen.
92         *
93         * @param detector The detector reporting the event - use this to
94         *          retrieve extended info about event state.
95         */
96        public void onScaleEnd(ScaleGestureDetector detector);
97    }
98
99    /**
100     * A convenience class to extend when you only want to listen for a subset
101     * of scaling-related events. This implements all methods in
102     * {@link OnScaleGestureListener} but does nothing.
103     * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
104     * {@code false} so that a subclass can retrieve the accumulated scale
105     * factor in an overridden onScaleEnd.
106     * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
107     * {@code true}.
108     */
109    public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
110
111        public boolean onScale(ScaleGestureDetector detector) {
112            return false;
113        }
114
115        public boolean onScaleBegin(ScaleGestureDetector detector) {
116            return true;
117        }
118
119        public void onScaleEnd(ScaleGestureDetector detector) {
120            // Intentionally empty
121        }
122    }
123
124    private final Context mContext;
125    private final OnScaleGestureListener mListener;
126
127    private float mFocusX;
128    private float mFocusY;
129
130    private float mCurrSpan;
131    private float mPrevSpan;
132    private float mInitialSpan;
133    private float mCurrSpanX;
134    private float mCurrSpanY;
135    private float mPrevSpanX;
136    private float mPrevSpanY;
137    private long mCurrTime;
138    private long mPrevTime;
139    private boolean mInProgress;
140    private int mSpanSlop;
141    private int mMinSpan;
142
143    // Bounds for recently seen values
144    private float mTouchUpper;
145    private float mTouchLower;
146    private float mTouchHistoryLastAccepted;
147    private int mTouchHistoryDirection;
148    private long mTouchHistoryLastAcceptedTime;
149    private int mTouchMinMajor;
150
151    private static final long TOUCH_STABILIZE_TIME = 128; // ms
152    private static final int TOUCH_MIN_MAJOR = 48; // dp
153
154    /**
155     * Consistency verifier for debugging purposes.
156     */
157    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
158            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
159                    new InputEventConsistencyVerifier(this, 0) : null;
160
161    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
162        mContext = context;
163        mListener = listener;
164        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
165        mTouchMinMajor =
166                (int) (context.getResources().getDisplayMetrics().density * TOUCH_MIN_MAJOR + 0.5f);
167        mMinSpan = context.getResources().getDimensionPixelSize(
168                com.android.internal.R.dimen.config_minScalingSpan);
169    }
170
171    /**
172     * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on
173     * some hardware/driver combos. Smooth it out to get kinder, gentler behavior.
174     * @param ev MotionEvent to add to the ongoing history
175     */
176    private void addTouchHistory(MotionEvent ev) {
177        final long currentTime = SystemClock.uptimeMillis();
178        final int count = ev.getPointerCount();
179        boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
180        float total = 0;
181        int sampleCount = 0;
182        for (int i = 0; i < count; i++) {
183            final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
184            final int historySize = ev.getHistorySize();
185            final int pointerSampleCount = historySize + 1;
186            for (int h = 0; h < pointerSampleCount; h++) {
187                float major;
188                if (h < historySize) {
189                    major = ev.getHistoricalTouchMajor(i, h);
190                } else {
191                    major = ev.getTouchMajor(i);
192                }
193                if (major < mTouchMinMajor) major = mTouchMinMajor;
194                total += major;
195
196                if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
197                    mTouchUpper = major;
198                }
199                if (Float.isNaN(mTouchLower) || major < mTouchLower) {
200                    mTouchLower = major;
201                }
202
203                if (hasLastAccepted) {
204                    final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
205                    if (directionSig != mTouchHistoryDirection ||
206                            (directionSig == 0 && mTouchHistoryDirection == 0)) {
207                        mTouchHistoryDirection = directionSig;
208                        final long time = h < historySize ? ev.getHistoricalEventTime(h)
209                                : ev.getEventTime();
210                        mTouchHistoryLastAcceptedTime = time;
211                        accept = false;
212                    }
213                }
214            }
215            sampleCount += pointerSampleCount;
216        }
217
218        final float avg = total / sampleCount;
219
220        if (accept) {
221            float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
222            mTouchUpper = (mTouchUpper + newAccepted) / 2;
223            mTouchLower = (mTouchLower + newAccepted) / 2;
224            mTouchHistoryLastAccepted = newAccepted;
225            mTouchHistoryDirection = 0;
226            mTouchHistoryLastAcceptedTime = ev.getEventTime();
227        }
228    }
229
230    /**
231     * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.
232     * @see #addTouchHistory(MotionEvent)
233     */
234    private void clearTouchHistory() {
235        mTouchUpper = Float.NaN;
236        mTouchLower = Float.NaN;
237        mTouchHistoryLastAccepted = Float.NaN;
238        mTouchHistoryDirection = 0;
239        mTouchHistoryLastAcceptedTime = 0;
240    }
241
242    /**
243     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
244     * when appropriate.
245     *
246     * <p>Applications should pass a complete and consistent event stream to this method.
247     * A complete and consistent event stream involves all MotionEvents from the initial
248     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
249     *
250     * @param event The event to process
251     * @return true if the event was processed and the detector wants to receive the
252     *         rest of the MotionEvents in this event stream.
253     */
254    public boolean onTouchEvent(MotionEvent event) {
255        if (mInputEventConsistencyVerifier != null) {
256            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
257        }
258
259        final int action = event.getActionMasked();
260
261        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
262                action == MotionEvent.ACTION_CANCEL;
263        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
264            // Reset any scale in progress with the listener.
265            // If it's an ACTION_DOWN we're beginning a new event stream.
266            // This means the app probably didn't give us all the events. Shame on it.
267            if (mInProgress) {
268                mListener.onScaleEnd(this);
269                mInProgress = false;
270                mInitialSpan = 0;
271            }
272
273            if (streamComplete) {
274                clearTouchHistory();
275                return true;
276            }
277        }
278
279        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
280                action == MotionEvent.ACTION_POINTER_UP ||
281                action == MotionEvent.ACTION_POINTER_DOWN;
282        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
283        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
284
285        // Determine focal point
286        float sumX = 0, sumY = 0;
287        final int count = event.getPointerCount();
288        for (int i = 0; i < count; i++) {
289            if (skipIndex == i) continue;
290            sumX += event.getX(i);
291            sumY += event.getY(i);
292        }
293        final int div = pointerUp ? count - 1 : count;
294        final float focusX = sumX / div;
295        final float focusY = sumY / div;
296
297
298        addTouchHistory(event);
299
300        // Determine average deviation from focal point
301        float devSumX = 0, devSumY = 0;
302        for (int i = 0; i < count; i++) {
303            if (skipIndex == i) continue;
304
305            // Convert the resulting diameter into a radius.
306            final float touchSize = mTouchHistoryLastAccepted / 2;
307            devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
308            devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
309        }
310        final float devX = devSumX / div;
311        final float devY = devSumY / div;
312
313        // Span is the average distance between touch points through the focal point;
314        // i.e. the diameter of the circle with a radius of the average deviation from
315        // the focal point.
316        final float spanX = devX * 2;
317        final float spanY = devY * 2;
318        final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
319
320        // Dispatch begin/end events as needed.
321        // If the configuration changes, notify the app to reset its current state by beginning
322        // a fresh scale event stream.
323        final boolean wasInProgress = mInProgress;
324        mFocusX = focusX;
325        mFocusY = focusY;
326        if (mInProgress && (span < mMinSpan || configChanged)) {
327            mListener.onScaleEnd(this);
328            mInProgress = false;
329            mInitialSpan = span;
330        }
331        if (configChanged) {
332            mPrevSpanX = mCurrSpanX = spanX;
333            mPrevSpanY = mCurrSpanY = spanY;
334            mInitialSpan = mPrevSpan = mCurrSpan = span;
335        }
336        if (!mInProgress && span >= mMinSpan &&
337                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
338            mPrevSpanX = mCurrSpanX = spanX;
339            mPrevSpanY = mCurrSpanY = spanY;
340            mPrevSpan = mCurrSpan = span;
341            mInProgress = mListener.onScaleBegin(this);
342        }
343
344        // Handle motion; focal point and span/scale factor are changing.
345        if (action == MotionEvent.ACTION_MOVE) {
346            mCurrSpanX = spanX;
347            mCurrSpanY = spanY;
348            mCurrSpan = span;
349
350            boolean updatePrev = true;
351            if (mInProgress) {
352                updatePrev = mListener.onScale(this);
353            }
354
355            if (updatePrev) {
356                mPrevSpanX = mCurrSpanX;
357                mPrevSpanY = mCurrSpanY;
358                mPrevSpan = mCurrSpan;
359            }
360        }
361
362        return true;
363    }
364
365    /**
366     * Returns {@code true} if a scale gesture is in progress.
367     */
368    public boolean isInProgress() {
369        return mInProgress;
370    }
371
372    /**
373     * Get the X coordinate of the current gesture's focal point.
374     * If a gesture is in progress, the focal point is between
375     * each of the pointers forming the gesture.
376     *
377     * If {@link #isInProgress()} would return false, the result of this
378     * function is undefined.
379     *
380     * @return X coordinate of the focal point in pixels.
381     */
382    public float getFocusX() {
383        return mFocusX;
384    }
385
386    /**
387     * Get the Y coordinate of the current gesture's focal point.
388     * If a gesture is in progress, the focal point is between
389     * each of the pointers forming the gesture.
390     *
391     * If {@link #isInProgress()} would return false, the result of this
392     * function is undefined.
393     *
394     * @return Y coordinate of the focal point in pixels.
395     */
396    public float getFocusY() {
397        return mFocusY;
398    }
399
400    /**
401     * Return the average distance between each of the pointers forming the
402     * gesture in progress through the focal point.
403     *
404     * @return Distance between pointers in pixels.
405     */
406    public float getCurrentSpan() {
407        return mCurrSpan;
408    }
409
410    /**
411     * Return the average X distance between each of the pointers forming the
412     * gesture in progress through the focal point.
413     *
414     * @return Distance between pointers in pixels.
415     */
416    public float getCurrentSpanX() {
417        return mCurrSpanX;
418    }
419
420    /**
421     * Return the average Y distance between each of the pointers forming the
422     * gesture in progress through the focal point.
423     *
424     * @return Distance between pointers in pixels.
425     */
426    public float getCurrentSpanY() {
427        return mCurrSpanY;
428    }
429
430    /**
431     * Return the previous average distance between each of the pointers forming the
432     * gesture in progress through the focal point.
433     *
434     * @return Previous distance between pointers in pixels.
435     */
436    public float getPreviousSpan() {
437        return mPrevSpan;
438    }
439
440    /**
441     * Return the previous average X distance between each of the pointers forming the
442     * gesture in progress through the focal point.
443     *
444     * @return Previous distance between pointers in pixels.
445     */
446    public float getPreviousSpanX() {
447        return mPrevSpanX;
448    }
449
450    /**
451     * Return the previous average Y distance between each of the pointers forming the
452     * gesture in progress through the focal point.
453     *
454     * @return Previous distance between pointers in pixels.
455     */
456    public float getPreviousSpanY() {
457        return mPrevSpanY;
458    }
459
460    /**
461     * Return the scaling factor from the previous scale event to the current
462     * event. This value is defined as
463     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
464     *
465     * @return The current scaling factor.
466     */
467    public float getScaleFactor() {
468        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
469    }
470
471    /**
472     * Return the time difference in milliseconds between the previous
473     * accepted scaling event and the current scaling event.
474     *
475     * @return Time difference since the last scaling event in milliseconds.
476     */
477    public long getTimeDelta() {
478        return mCurrTime - mPrevTime;
479    }
480
481    /**
482     * Return the event time of the current event being processed.
483     *
484     * @return Current event time in milliseconds.
485     */
486    public long getEventTime() {
487        return mCurrTime;
488    }
489}
490