2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 import android.content.Context;
20 import android.util.FloatMath;
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.
27 * This class should only be used with {@link MotionEvent}s reported via touch.
31 * <li>Create an instance of the {@code ScaleGestureDetector} for your
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.
38 public class ScaleGestureDetector {
39 private static final String TAG = "ScaleGestureDetector";
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}.
47 * An application will receive events in the following order:
49 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
50 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
51 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
54 public interface OnScaleGestureListener {
56 * Responds to scaling events for a gesture in progress.
57 * Reported by pointer motion.
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
68 public boolean onScale(ScaleGestureDetector detector);
71 * Responds to the beginning of a scaling gesture. Reported by
72 * new pointers going down.
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.
82 public boolean onScaleBegin(ScaleGestureDetector detector);
85 * Responds to the end of a scale gesture. Reported by existing
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.
92 * @param detector The detector reporting the event - use this to
93 * retrieve extended info about event state.
95 public void onScaleEnd(ScaleGestureDetector detector);
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
108 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
110 public boolean onScale(ScaleGestureDetector detector) {
114 public boolean onScaleBegin(ScaleGestureDetector detector) {
118 public void onScaleEnd(ScaleGestureDetector detector) {
119 // Intentionally empty
123 private final Context mContext;
124 private final OnScaleGestureListener mListener;
126 private float mFocusX;
127 private float mFocusY;
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;
142 * Consistency verifier for debugging purposes.
144 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
145 InputEventConsistencyVerifier.isInstrumentationEnabled() ?
146 new InputEventConsistencyVerifier(this, 0) : null;
148 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
150 mListener = listener;
151 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
155 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
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>
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.
166 public boolean onTouchEvent(MotionEvent event) {
167 if (mInputEventConsistencyVerifier != null) {
168 mInputEventConsistencyVerifier.onTouchEvent(event, 0);
171 final int action = event.getActionMasked();
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.
180 mListener.onScaleEnd(this);
185 if (streamComplete) {
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;
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);
204 final int div = pointerUp ? count - 1 : count;
205 final float focusX = sumX / div;
206 final float focusY = sumY / div;
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);
215 final float devX = devSumX / div;
216 final float devY = devSumY / div;
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
221 final float spanX = devX * 2;
222 final float spanY = devY * 2;
223 final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
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;
231 if (mInProgress && (span == 0 || configChanged)) {
232 mListener.onScaleEnd(this);
237 mPrevSpanX = mCurrSpanX = spanX;
238 mPrevSpanY = mCurrSpanY = spanY;
239 mInitialSpan = mPrevSpan = mCurrSpan = span;
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);
249 // Handle motion; focal point and span/scale factor are changing.
250 if (action == MotionEvent.ACTION_MOVE) {
255 boolean updatePrev = true;
257 updatePrev = mListener.onScale(this);
261 mPrevSpanX = mCurrSpanX;
262 mPrevSpanY = mCurrSpanY;
263 mPrevSpan = mCurrSpan;
271 * Returns {@code true} if a scale gesture is in progress.
273 public boolean isInProgress() {
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.
282 * If {@link #isInProgress()} would return false, the result of this
283 * function is undefined.
285 * @return X coordinate of the focal point in pixels.
287 public float getFocusX() {
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.
296 * If {@link #isInProgress()} would return false, the result of this
297 * function is undefined.
299 * @return Y coordinate of the focal point in pixels.
301 public float getFocusY() {
306 * Return the average distance between each of the pointers forming the
307 * gesture in progress through the focal point.
309 * @return Distance between pointers in pixels.
311 public float getCurrentSpan() {
316 * Return the average X distance between each of the pointers forming the
317 * gesture in progress through the focal point.
319 * @return Distance between pointers in pixels.
321 public float getCurrentSpanX() {
326 * Return the average Y distance between each of the pointers forming the
327 * gesture in progress through the focal point.
329 * @return Distance between pointers in pixels.
331 public float getCurrentSpanY() {
336 * Return the previous average distance between each of the pointers forming the
337 * gesture in progress through the focal point.
339 * @return Previous distance between pointers in pixels.
341 public float getPreviousSpan() {
346 * Return the previous average X distance between each of the pointers forming the
347 * gesture in progress through the focal point.
349 * @return Previous distance between pointers in pixels.
351 public float getPreviousSpanX() {
356 * Return the previous average Y distance between each of the pointers forming the
357 * gesture in progress through the focal point.
359 * @return Previous distance between pointers in pixels.
361 public float getPreviousSpanY() {
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()}).
370 * @return The current scaling factor.
372 public float getScaleFactor() {
373 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
377 * Return the time difference in milliseconds between the previous
378 * accepted scaling event and the current scaling event.
380 * @return Time difference since the last scaling event in milliseconds.
382 public long getTimeDelta() {
383 return mCurrTime - mPrevTime;
387 * Return the event time of the current event being processed.
389 * @return Current event time in milliseconds.
391 public long getEventTime() {