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