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