ScaleGestureDetector.java revision ae542ff055301a4c3c8a18e8da1739df3a771958
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;
20
21/**
22 * Detects transformation gestures involving more than one pointer ("multitouch")
23 * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
24 * callback will notify users when a particular gesture event has occurred.
25 * This class should only be used with {@link MotionEvent}s reported via touch.
26 *
27 * To use this class:
28 * <ul>
29 *  <li>Create an instance of the {@code ScaleGestureDetector} for your
30 *      {@link View}
31 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
32 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your
33 *          callback will be executed when the events occur.
34 * </ul>
35 * @hide Pending API approval
36 */
37public class ScaleGestureDetector {
38    /**
39     * The listener for receiving notifications when gestures occur.
40     * If you want to listen for all the different gestures then implement
41     * this interface. If you only want to listen for a subset it might
42     * be easier to extend {@link SimpleOnScaleGestureListener}.
43     *
44     * An application will receive events in the following order:
45     * <ul>
46     *  <li>One {@link OnScaleGestureListener#onScaleBegin()}
47     *  <li>Zero or more {@link OnScaleGestureListener#onScale()}
48     *  <li>One {@link OnScaleGestureListener#onTransformEnd()}
49     * </ul>
50     */
51    public interface OnScaleGestureListener {
52        /**
53         * Responds to scaling events for a gesture in progress.
54         * Reported by pointer motion.
55         *
56         * @param detector The detector reporting the event - use this to
57         *          retrieve extended info about event state.
58         * @return Whether or not the detector should consider this event
59         *          as handled. If an event was not handled, the detector
60         *          will continue to accumulate movement until an event is
61         *          handled. This can be useful if an application, for example,
62         *          only wants to update scaling factors if the change is
63         *          greater than 0.01.
64         */
65        public boolean onScale(ScaleGestureDetector detector);
66
67        /**
68         * Responds to the beginning of a scaling gesture. Reported by
69         * new pointers going down.
70         *
71         * @param detector The detector reporting the event - use this to
72         *          retrieve extended info about event state.
73         * @return Whether or not the detector should continue recognizing
74         *          this gesture. For example, if a gesture is beginning
75         *          with a focal point outside of a region where it makes
76         *          sense, onScaleBegin() may return false to ignore the
77         *          rest of the gesture.
78         */
79        public boolean onScaleBegin(ScaleGestureDetector detector);
80
81        /**
82         * Responds to the end of a scale gesture. Reported by existing
83         * pointers going up. If the end of a gesture would result in a fling,
84         * {@link onTransformFling()} is called instead.
85         *
86         * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
87         * and {@link ScaleGestureDetector#getFocusY()} will return the location
88         * of the pointer remaining on the screen.
89         *
90         * @param detector The detector reporting the event - use this to
91         *          retrieve extended info about event state.
92         */
93        public void onScaleEnd(ScaleGestureDetector detector);
94    }
95
96    /**
97     * A convenience class to extend when you only want to listen for a subset
98     * of scaling-related events. This implements all methods in
99     * {@link OnScaleGestureListener} but does nothing.
100     * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} and
101     * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} return
102     * {@code true}.
103     */
104    public class SimpleOnScaleGestureListener implements OnScaleGestureListener {
105
106        public boolean onScale(ScaleGestureDetector detector) {
107            return true;
108        }
109
110        public boolean onScaleBegin(ScaleGestureDetector detector) {
111            return true;
112        }
113
114        public void onScaleEnd(ScaleGestureDetector detector) {
115            // Intentionally empty
116        }
117    }
118
119    private static final float PRESSURE_THRESHOLD = 0.67f;
120
121    private Context mContext;
122    private OnScaleGestureListener mListener;
123    private boolean mGestureInProgress;
124
125    private MotionEvent mPrevEvent;
126    private MotionEvent mCurrEvent;
127
128    private float mFocusX;
129    private float mFocusY;
130    private float mPrevFingerDiffX;
131    private float mPrevFingerDiffY;
132    private float mCurrFingerDiffX;
133    private float mCurrFingerDiffY;
134    private float mCurrLen;
135    private float mPrevLen;
136    private float mScaleFactor;
137    private float mCurrPressure;
138    private float mPrevPressure;
139    private long mTimeDelta;
140
141    public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
142        mContext = context;
143        mListener = listener;
144    }
145
146    public boolean onTouchEvent(MotionEvent event) {
147        final int action = event.getAction();
148        boolean handled = true;
149
150        if (!mGestureInProgress) {
151            if ((action == MotionEvent.ACTION_POINTER_1_DOWN ||
152                    action == MotionEvent.ACTION_POINTER_2_DOWN) &&
153                    event.getPointerCount() >= 2) {
154                // We have a new multi-finger gesture
155
156                // Be paranoid in case we missed an event
157                reset();
158
159                mPrevEvent = MotionEvent.obtain(event);
160                mTimeDelta = 0;
161
162                setContext(event);
163                mGestureInProgress = mListener.onScaleBegin(this);
164            }
165        } else {
166            // Transform gesture in progress - attempt to handle it
167            switch (action) {
168                case MotionEvent.ACTION_POINTER_1_UP:
169                case MotionEvent.ACTION_POINTER_2_UP:
170                    // Gesture ended
171                    setContext(event);
172
173                    // Set focus point to the remaining finger
174                    int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK)
175                            >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0;
176                    mFocusX = event.getX(id);
177                    mFocusY = event.getY(id);
178
179                    mListener.onScaleEnd(this);
180                    mGestureInProgress = false;
181
182                    reset();
183                    break;
184
185                case MotionEvent.ACTION_CANCEL:
186                    mListener.onScaleEnd(this);
187                    mGestureInProgress = false;
188
189                    reset();
190                    break;
191
192                case MotionEvent.ACTION_MOVE:
193                    setContext(event);
194
195                    // Only accept the event if our relative pressure is within
196                    // a certain limit - this can help filter shaky data as a
197                    // finger is lifted.
198                    if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
199                        final boolean updatePrevious = mListener.onScale(this);
200
201                        if (updatePrevious) {
202                            mPrevEvent.recycle();
203                            mPrevEvent = MotionEvent.obtain(event);
204                        }
205                    }
206                    break;
207            }
208        }
209        return handled;
210    }
211
212    private void setContext(MotionEvent curr) {
213        if (mCurrEvent != null) {
214            mCurrEvent.recycle();
215        }
216        mCurrEvent = MotionEvent.obtain(curr);
217
218        mCurrLen = -1;
219        mPrevLen = -1;
220        mScaleFactor = -1;
221
222        final MotionEvent prev = mPrevEvent;
223
224        final float px0 = prev.getX(0);
225        final float py0 = prev.getY(0);
226        final float px1 = prev.getX(1);
227        final float py1 = prev.getY(1);
228        final float cx0 = curr.getX(0);
229        final float cy0 = curr.getY(0);
230        final float cx1 = curr.getX(1);
231        final float cy1 = curr.getY(1);
232
233        final float pvx = px1 - px0;
234        final float pvy = py1 - py0;
235        final float cvx = cx1 - cx0;
236        final float cvy = cy1 - cy0;
237        mPrevFingerDiffX = pvx;
238        mPrevFingerDiffY = pvy;
239        mCurrFingerDiffX = cvx;
240        mCurrFingerDiffY = cvy;
241
242        mFocusX = cx0 + cvx * 0.5f;
243        mFocusY = cy0 + cvy * 0.5f;
244        mTimeDelta = curr.getEventTime() - prev.getEventTime();
245        mCurrPressure = curr.getPressure(0) + curr.getPressure(1);
246        mPrevPressure = prev.getPressure(0) + prev.getPressure(1);
247    }
248
249    private void reset() {
250        if (mPrevEvent != null) {
251            mPrevEvent.recycle();
252            mPrevEvent = null;
253        }
254        if (mCurrEvent != null) {
255            mCurrEvent.recycle();
256            mCurrEvent = null;
257        }
258    }
259
260    /**
261     * Returns {@code true} if a two-finger scale gesture is in progress.
262     * @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
263     */
264    public boolean isInProgress() {
265        return mGestureInProgress;
266    }
267
268    /**
269     * Get the X coordinate of the current gesture's focal point.
270     * If a gesture is in progress, the focal point is directly between
271     * the two pointers forming the gesture.
272     * If a gesture is ending, the focal point is the location of the
273     * remaining pointer on the screen.
274     * If {@link isInProgress()} would return false, the result of this
275     * function is undefined.
276     *
277     * @return X coordinate of the focal point in pixels.
278     */
279    public float getFocusX() {
280        return mFocusX;
281    }
282
283    /**
284     * Get the Y coordinate of the current gesture's focal point.
285     * If a gesture is in progress, the focal point is directly between
286     * the two pointers forming the gesture.
287     * If a gesture is ending, the focal point is the location of the
288     * remaining pointer on the screen.
289     * If {@link isInProgress()} would return false, the result of this
290     * function is undefined.
291     *
292     * @return Y coordinate of the focal point in pixels.
293     */
294    public float getFocusY() {
295        return mFocusY;
296    }
297
298    /**
299     * Return the current distance between the two pointers forming the
300     * gesture in progress.
301     *
302     * @return Distance between pointers in pixels.
303     */
304    public float getCurrentSpan() {
305        if (mCurrLen == -1) {
306            final float cvx = mCurrFingerDiffX;
307            final float cvy = mCurrFingerDiffY;
308            mCurrLen = (float)Math.sqrt(cvx*cvx + cvy*cvy);
309        }
310        return mCurrLen;
311    }
312
313    /**
314     * Return the previous distance between the two pointers forming the
315     * gesture in progress.
316     *
317     * @return Previous distance between pointers in pixels.
318     */
319    public float getPreviousSpan() {
320        if (mPrevLen == -1) {
321            final float pvx = mPrevFingerDiffX;
322            final float pvy = mPrevFingerDiffY;
323            mPrevLen = (float)Math.sqrt(pvx*pvx + pvy*pvy);
324        }
325        return mPrevLen;
326    }
327
328    /**
329     * Return the scaling factor from the previous scale event to the current
330     * event. This value is defined as
331     * ({@link getCurrentSpan()} / {@link getPreviousSpan()}).
332     *
333     * @return The current scaling factor.
334     */
335    public float getScaleFactor() {
336        if (mScaleFactor == -1) {
337            mScaleFactor = getCurrentSpan() / getPreviousSpan();
338        }
339        return mScaleFactor;
340    }
341
342    /**
343     * Return the time difference in milliseconds between the previous
344     * accepted scaling event and the current scaling event.
345     *
346     * @return Time difference since the last scaling event in milliseconds.
347     */
348    public long getTimeDelta() {
349        return mTimeDelta;
350    }
351
352    /**
353     * Return the event time of the current event being processed.
354     *
355     * @return Current event time in milliseconds.
356     */
357    public long getEventTime() {
358        return mCurrEvent.getEventTime();
359    }
360}
361