ScaleGestureDetector.java revision 47ec2fb37046797bebc82b505a13c552021b9ff3
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
141    /**
142     * Consistency verifier for debugging purposes.
143     */
144    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
145            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
146                    new InputEventConsistencyVerifier(this, 0) : null;
147
148    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
149        mContext = context;
150        mListener = listener;
151        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
152    }
153
154    /**
155     * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
156     * when appropriate.
157     *
158     * <p>Applications should pass a complete and consistent event stream to this method.
159     * A complete and consistent event stream involves all MotionEvents from the initial
160     * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p>
161     *
162     * @param event The event to process
163     * @return true if the event was processed and the detector wants to receive the
164     *         rest of the MotionEvents in this event stream.
165     */
166    public boolean onTouchEvent(MotionEvent event) {
167        if (mInputEventConsistencyVerifier != null) {
168            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
169        }
170
171        final int action = event.getActionMasked();
172
173        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
174                action == MotionEvent.ACTION_CANCEL;
175        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
176            // Reset any scale in progress with the listener.
177            // If it's an ACTION_DOWN we're beginning a new event stream.
178            // This means the app probably didn't give us all the events. Shame on it.
179            if (mInProgress) {
180                mListener.onScaleEnd(this);
181                mInProgress = false;
182                mInitialSpan = 0;
183            }
184
185            if (streamComplete) {
186                return true;
187            }
188        }
189
190        final boolean configChanged =
191                action == MotionEvent.ACTION_POINTER_UP ||
192                action == MotionEvent.ACTION_POINTER_DOWN;
193        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
194        final int skipIndex = pointerUp ? event.getActionIndex() : -1;
195
196        // Determine focal point
197        float sumX = 0, sumY = 0;
198        final int count = event.getPointerCount();
199        for (int i = 0; i < count; i++) {
200            if (skipIndex == i) continue;
201            sumX += event.getX(i);
202            sumY += event.getY(i);
203        }
204        final int div = pointerUp ? count - 1 : count;
205        final float focusX = sumX / div;
206        final float focusY = sumY / div;
207
208        // Determine average deviation from focal point
209        float devSumX = 0, devSumY = 0;
210        for (int i = 0; i < count; i++) {
211            if (skipIndex == i) continue;
212            devSumX += Math.abs(event.getX(i) - focusX);
213            devSumY += Math.abs(event.getY(i) - focusY);
214        }
215        final float devX = devSumX / div;
216        final float devY = devSumY / div;
217
218        // Span is the average distance between touch points through the focal point;
219        // i.e. the diameter of the circle with a radius of the average deviation from
220        // the focal point.
221        final float spanX = devX * 2;
222        final float spanY = devY * 2;
223        final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
224
225        // Dispatch begin/end events as needed.
226        // If the configuration changes, notify the app to reset its current state by beginning
227        // a fresh scale event stream.
228        final boolean wasInProgress = mInProgress;
229        mFocusX = focusX;
230        mFocusY = focusY;
231        if (mInProgress && (span == 0 || configChanged)) {
232            mListener.onScaleEnd(this);
233            mInProgress = false;
234            mInitialSpan = span;
235        }
236        if (configChanged) {
237            mPrevSpanX = mCurrSpanX = spanX;
238            mPrevSpanY = mCurrSpanY = spanY;
239            mInitialSpan = mPrevSpan = mCurrSpan = span;
240        }
241        if (!mInProgress && span != 0 &&
242                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
243            mPrevSpanX = mCurrSpanX = spanX;
244            mPrevSpanY = mCurrSpanY = spanY;
245            mPrevSpan = mCurrSpan = span;
246            mInProgress = mListener.onScaleBegin(this);
247        }
248
249        // Handle motion; focal point and span/scale factor are changing.
250        if (action == MotionEvent.ACTION_MOVE) {
251            mCurrSpanX = spanX;
252            mCurrSpanY = spanY;
253            mCurrSpan = span;
254
255            boolean updatePrev = true;
256            if (mInProgress) {
257                updatePrev = mListener.onScale(this);
258            }
259
260            if (updatePrev) {
261                mPrevSpanX = mCurrSpanX;
262                mPrevSpanY = mCurrSpanY;
263                mPrevSpan = mCurrSpan;
264            }
265        }
266
267        return true;
268    }
269
270    /**
271     * Returns {@code true} if a scale gesture is in progress.
272     */
273    public boolean isInProgress() {
274        return mInProgress;
275    }
276
277    /**
278     * Get the X coordinate of the current gesture's focal point.
279     * If a gesture is in progress, the focal point is between
280     * each of the pointers forming the gesture.
281     *
282     * If {@link #isInProgress()} would return false, the result of this
283     * function is undefined.
284     *
285     * @return X coordinate of the focal point in pixels.
286     */
287    public float getFocusX() {
288        return mFocusX;
289    }
290
291    /**
292     * Get the Y coordinate of the current gesture's focal point.
293     * If a gesture is in progress, the focal point is between
294     * each of the pointers forming the gesture.
295     *
296     * If {@link #isInProgress()} would return false, the result of this
297     * function is undefined.
298     *
299     * @return Y coordinate of the focal point in pixels.
300     */
301    public float getFocusY() {
302        return mFocusY;
303    }
304
305    /**
306     * Return the average distance between each of the pointers forming the
307     * gesture in progress through the focal point.
308     *
309     * @return Distance between pointers in pixels.
310     */
311    public float getCurrentSpan() {
312        return mCurrSpan;
313    }
314
315    /**
316     * Return the average X distance between each of the pointers forming the
317     * gesture in progress through the focal point.
318     *
319     * @return Distance between pointers in pixels.
320     */
321    public float getCurrentSpanX() {
322        return mCurrSpanX;
323    }
324
325    /**
326     * Return the average Y distance between each of the pointers forming the
327     * gesture in progress through the focal point.
328     *
329     * @return Distance between pointers in pixels.
330     */
331    public float getCurrentSpanY() {
332        return mCurrSpanY;
333    }
334
335    /**
336     * Return the previous average distance between each of the pointers forming the
337     * gesture in progress through the focal point.
338     *
339     * @return Previous distance between pointers in pixels.
340     */
341    public float getPreviousSpan() {
342        return mPrevSpan;
343    }
344
345    /**
346     * Return the previous average X distance between each of the pointers forming the
347     * gesture in progress through the focal point.
348     *
349     * @return Previous distance between pointers in pixels.
350     */
351    public float getPreviousSpanX() {
352        return mPrevSpanX;
353    }
354
355    /**
356     * Return the previous average Y distance between each of the pointers forming the
357     * gesture in progress through the focal point.
358     *
359     * @return Previous distance between pointers in pixels.
360     */
361    public float getPreviousSpanY() {
362        return mPrevSpanY;
363    }
364
365    /**
366     * Return the scaling factor from the previous scale event to the current
367     * event. This value is defined as
368     * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).
369     *
370     * @return The current scaling factor.
371     */
372    public float getScaleFactor() {
373        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
374    }
375
376    /**
377     * Return the time difference in milliseconds between the previous
378     * accepted scaling event and the current scaling event.
379     *
380     * @return Time difference since the last scaling event in milliseconds.
381     */
382    public long getTimeDelta() {
383        return mCurrTime - mPrevTime;
384    }
385
386    /**
387     * Return the event time of the current event being processed.
388     *
389     * @return Current event time in milliseconds.
390     */
391    public long getEventTime() {
392        return mCurrTime;
393    }
394}
395