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