ScaleGestureDetector.java revision 5b5c414e31c4a8433a3290b931687a05dadc97b6
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.os.SystemClock; 21import android.util.FloatMath; 22 23/** 24 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s. 25 * The {@link OnScaleGestureListener} callback will notify users when a particular 26 * gesture event has occurred. 27 * 28 * This class should only be used with {@link MotionEvent}s reported via touch. 29 * 30 * To use this class: 31 * <ul> 32 * <li>Create an instance of the {@code ScaleGestureDetector} for your 33 * {@link View} 34 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 35 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 36 * callback will be executed when the events occur. 37 * </ul> 38 */ 39public class ScaleGestureDetector { 40 private static final String TAG = "ScaleGestureDetector"; 41 42 /** 43 * The listener for receiving notifications when gestures occur. 44 * If you want to listen for all the different gestures then implement 45 * this interface. If you only want to listen for a subset it might 46 * be easier to extend {@link SimpleOnScaleGestureListener}. 47 * 48 * An application will receive events in the following order: 49 * <ul> 50 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 51 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 52 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 53 * </ul> 54 */ 55 public interface OnScaleGestureListener { 56 /** 57 * Responds to scaling events for a gesture in progress. 58 * Reported by pointer motion. 59 * 60 * @param detector The detector reporting the event - use this to 61 * retrieve extended info about event state. 62 * @return Whether or not the detector should consider this event 63 * as handled. If an event was not handled, the detector 64 * will continue to accumulate movement until an event is 65 * handled. This can be useful if an application, for example, 66 * only wants to update scaling factors if the change is 67 * greater than 0.01. 68 */ 69 public boolean onScale(ScaleGestureDetector detector); 70 71 /** 72 * Responds to the beginning of a scaling gesture. Reported by 73 * new pointers going down. 74 * 75 * @param detector The detector reporting the event - use this to 76 * retrieve extended info about event state. 77 * @return Whether or not the detector should continue recognizing 78 * this gesture. For example, if a gesture is beginning 79 * with a focal point outside of a region where it makes 80 * sense, onScaleBegin() may return false to ignore the 81 * rest of the gesture. 82 */ 83 public boolean onScaleBegin(ScaleGestureDetector detector); 84 85 /** 86 * Responds to the end of a scale gesture. Reported by existing 87 * pointers going up. 88 * 89 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 90 * and {@link ScaleGestureDetector#getFocusY()} will return focal point 91 * of the pointers remaining on the screen. 92 * 93 * @param detector The detector reporting the event - use this to 94 * retrieve extended info about event state. 95 */ 96 public void onScaleEnd(ScaleGestureDetector detector); 97 } 98 99 /** 100 * A convenience class to extend when you only want to listen for a subset 101 * of scaling-related events. This implements all methods in 102 * {@link OnScaleGestureListener} but does nothing. 103 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns 104 * {@code false} so that a subclass can retrieve the accumulated scale 105 * factor in an overridden onScaleEnd. 106 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns 107 * {@code true}. 108 */ 109 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { 110 111 public boolean onScale(ScaleGestureDetector detector) { 112 return false; 113 } 114 115 public boolean onScaleBegin(ScaleGestureDetector detector) { 116 return true; 117 } 118 119 public void onScaleEnd(ScaleGestureDetector detector) { 120 // Intentionally empty 121 } 122 } 123 124 private final Context mContext; 125 private final OnScaleGestureListener mListener; 126 127 private float mFocusX; 128 private float mFocusY; 129 130 private float mCurrSpan; 131 private float mPrevSpan; 132 private float mInitialSpan; 133 private float mCurrSpanX; 134 private float mCurrSpanY; 135 private float mPrevSpanX; 136 private float mPrevSpanY; 137 private long mCurrTime; 138 private long mPrevTime; 139 private boolean mInProgress; 140 private int mSpanSlop; 141 private int mMinSpan; 142 143 // Bounds for recently seen values 144 private float mTouchUpper; 145 private float mTouchLower; 146 private float mTouchHistoryLastAccepted; 147 private int mTouchHistoryDirection; 148 private long mTouchHistoryLastAcceptedTime; 149 private int mTouchMinMajor; 150 151 private static final long TOUCH_STABILIZE_TIME = 128; // ms 152 private static final int TOUCH_MIN_MAJOR = 48; // dp 153 154 /** 155 * Consistency verifier for debugging purposes. 156 */ 157 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = 158 InputEventConsistencyVerifier.isInstrumentationEnabled() ? 159 new InputEventConsistencyVerifier(this, 0) : null; 160 161 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 162 mContext = context; 163 mListener = listener; 164 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; 165 mTouchMinMajor = 166 (int) (context.getResources().getDisplayMetrics().density * TOUCH_MIN_MAJOR + 0.5f); 167 mMinSpan = context.getResources().getDimensionPixelSize( 168 com.android.internal.R.dimen.config_minScalingSpan); 169 } 170 171 /** 172 * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on 173 * some hardware/driver combos. Smooth it out to get kinder, gentler behavior. 174 * @param ev MotionEvent to add to the ongoing history 175 */ 176 private void addTouchHistory(MotionEvent ev) { 177 final long currentTime = SystemClock.uptimeMillis(); 178 final int count = ev.getPointerCount(); 179 boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME; 180 float total = 0; 181 int sampleCount = 0; 182 for (int i = 0; i < count; i++) { 183 final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted); 184 final int historySize = ev.getHistorySize(); 185 final int pointerSampleCount = historySize + 1; 186 for (int h = 0; h < pointerSampleCount; h++) { 187 float major; 188 if (h < historySize) { 189 major = ev.getHistoricalTouchMajor(i, h); 190 } else { 191 major = ev.getTouchMajor(i); 192 } 193 if (major < mTouchMinMajor) major = mTouchMinMajor; 194 total += major; 195 196 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) { 197 mTouchUpper = major; 198 } 199 if (Float.isNaN(mTouchLower) || major < mTouchLower) { 200 mTouchLower = major; 201 } 202 203 if (hasLastAccepted) { 204 final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted); 205 if (directionSig != mTouchHistoryDirection || 206 (directionSig == 0 && mTouchHistoryDirection == 0)) { 207 mTouchHistoryDirection = directionSig; 208 final long time = h < historySize ? ev.getHistoricalEventTime(h) 209 : ev.getEventTime(); 210 mTouchHistoryLastAcceptedTime = time; 211 accept = false; 212 } 213 } 214 } 215 sampleCount += pointerSampleCount; 216 } 217 218 final float avg = total / sampleCount; 219 220 if (accept) { 221 float newAccepted = (mTouchUpper + mTouchLower + avg) / 3; 222 mTouchUpper = (mTouchUpper + newAccepted) / 2; 223 mTouchLower = (mTouchLower + newAccepted) / 2; 224 mTouchHistoryLastAccepted = newAccepted; 225 mTouchHistoryDirection = 0; 226 mTouchHistoryLastAcceptedTime = ev.getEventTime(); 227 } 228 } 229 230 /** 231 * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP. 232 * @see #addTouchHistory(MotionEvent) 233 */ 234 private void clearTouchHistory() { 235 mTouchUpper = Float.NaN; 236 mTouchLower = Float.NaN; 237 mTouchHistoryLastAccepted = Float.NaN; 238 mTouchHistoryDirection = 0; 239 mTouchHistoryLastAcceptedTime = 0; 240 } 241 242 /** 243 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} 244 * when appropriate. 245 * 246 * <p>Applications should pass a complete and consistent event stream to this method. 247 * A complete and consistent event stream involves all MotionEvents from the initial 248 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> 249 * 250 * @param event The event to process 251 * @return true if the event was processed and the detector wants to receive the 252 * rest of the MotionEvents in this event stream. 253 */ 254 public boolean onTouchEvent(MotionEvent event) { 255 if (mInputEventConsistencyVerifier != null) { 256 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 257 } 258 259 final int action = event.getActionMasked(); 260 261 final boolean streamComplete = action == MotionEvent.ACTION_UP || 262 action == MotionEvent.ACTION_CANCEL; 263 if (action == MotionEvent.ACTION_DOWN || streamComplete) { 264 // Reset any scale in progress with the listener. 265 // If it's an ACTION_DOWN we're beginning a new event stream. 266 // This means the app probably didn't give us all the events. Shame on it. 267 if (mInProgress) { 268 mListener.onScaleEnd(this); 269 mInProgress = false; 270 mInitialSpan = 0; 271 } 272 273 if (streamComplete) { 274 clearTouchHistory(); 275 return true; 276 } 277 } 278 279 final boolean configChanged = action == MotionEvent.ACTION_DOWN || 280 action == MotionEvent.ACTION_POINTER_UP || 281 action == MotionEvent.ACTION_POINTER_DOWN; 282 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; 283 final int skipIndex = pointerUp ? event.getActionIndex() : -1; 284 285 // Determine focal point 286 float sumX = 0, sumY = 0; 287 final int count = event.getPointerCount(); 288 for (int i = 0; i < count; i++) { 289 if (skipIndex == i) continue; 290 sumX += event.getX(i); 291 sumY += event.getY(i); 292 } 293 final int div = pointerUp ? count - 1 : count; 294 final float focusX = sumX / div; 295 final float focusY = sumY / div; 296 297 298 addTouchHistory(event); 299 300 // Determine average deviation from focal point 301 float devSumX = 0, devSumY = 0; 302 for (int i = 0; i < count; i++) { 303 if (skipIndex == i) continue; 304 305 // Convert the resulting diameter into a radius. 306 final float touchSize = mTouchHistoryLastAccepted / 2; 307 devSumX += Math.abs(event.getX(i) - focusX) + touchSize; 308 devSumY += Math.abs(event.getY(i) - focusY) + touchSize; 309 } 310 final float devX = devSumX / div; 311 final float devY = devSumY / div; 312 313 // Span is the average distance between touch points through the focal point; 314 // i.e. the diameter of the circle with a radius of the average deviation from 315 // the focal point. 316 final float spanX = devX * 2; 317 final float spanY = devY * 2; 318 final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY); 319 320 // Dispatch begin/end events as needed. 321 // If the configuration changes, notify the app to reset its current state by beginning 322 // a fresh scale event stream. 323 final boolean wasInProgress = mInProgress; 324 mFocusX = focusX; 325 mFocusY = focusY; 326 if (mInProgress && (span < mMinSpan || configChanged)) { 327 mListener.onScaleEnd(this); 328 mInProgress = false; 329 mInitialSpan = span; 330 } 331 if (configChanged) { 332 mPrevSpanX = mCurrSpanX = spanX; 333 mPrevSpanY = mCurrSpanY = spanY; 334 mInitialSpan = mPrevSpan = mCurrSpan = span; 335 } 336 if (!mInProgress && span >= mMinSpan && 337 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) { 338 mPrevSpanX = mCurrSpanX = spanX; 339 mPrevSpanY = mCurrSpanY = spanY; 340 mPrevSpan = mCurrSpan = span; 341 mInProgress = mListener.onScaleBegin(this); 342 } 343 344 // Handle motion; focal point and span/scale factor are changing. 345 if (action == MotionEvent.ACTION_MOVE) { 346 mCurrSpanX = spanX; 347 mCurrSpanY = spanY; 348 mCurrSpan = span; 349 350 boolean updatePrev = true; 351 if (mInProgress) { 352 updatePrev = mListener.onScale(this); 353 } 354 355 if (updatePrev) { 356 mPrevSpanX = mCurrSpanX; 357 mPrevSpanY = mCurrSpanY; 358 mPrevSpan = mCurrSpan; 359 } 360 } 361 362 return true; 363 } 364 365 /** 366 * Returns {@code true} if a scale gesture is in progress. 367 */ 368 public boolean isInProgress() { 369 return mInProgress; 370 } 371 372 /** 373 * Get the X coordinate of the current gesture's focal point. 374 * If a gesture is in progress, the focal point is between 375 * each of the pointers forming the gesture. 376 * 377 * If {@link #isInProgress()} would return false, the result of this 378 * function is undefined. 379 * 380 * @return X coordinate of the focal point in pixels. 381 */ 382 public float getFocusX() { 383 return mFocusX; 384 } 385 386 /** 387 * Get the Y coordinate of the current gesture's focal point. 388 * If a gesture is in progress, the focal point is between 389 * each of the pointers forming the gesture. 390 * 391 * If {@link #isInProgress()} would return false, the result of this 392 * function is undefined. 393 * 394 * @return Y coordinate of the focal point in pixels. 395 */ 396 public float getFocusY() { 397 return mFocusY; 398 } 399 400 /** 401 * Return the average distance between each of the pointers forming the 402 * gesture in progress through the focal point. 403 * 404 * @return Distance between pointers in pixels. 405 */ 406 public float getCurrentSpan() { 407 return mCurrSpan; 408 } 409 410 /** 411 * Return the average X distance between each of the pointers forming the 412 * gesture in progress through the focal point. 413 * 414 * @return Distance between pointers in pixels. 415 */ 416 public float getCurrentSpanX() { 417 return mCurrSpanX; 418 } 419 420 /** 421 * Return the average Y distance between each of the pointers forming the 422 * gesture in progress through the focal point. 423 * 424 * @return Distance between pointers in pixels. 425 */ 426 public float getCurrentSpanY() { 427 return mCurrSpanY; 428 } 429 430 /** 431 * Return the previous average distance between each of the pointers forming the 432 * gesture in progress through the focal point. 433 * 434 * @return Previous distance between pointers in pixels. 435 */ 436 public float getPreviousSpan() { 437 return mPrevSpan; 438 } 439 440 /** 441 * Return the previous average X distance between each of the pointers forming the 442 * gesture in progress through the focal point. 443 * 444 * @return Previous distance between pointers in pixels. 445 */ 446 public float getPreviousSpanX() { 447 return mPrevSpanX; 448 } 449 450 /** 451 * Return the previous average Y distance between each of the pointers forming the 452 * gesture in progress through the focal point. 453 * 454 * @return Previous distance between pointers in pixels. 455 */ 456 public float getPreviousSpanY() { 457 return mPrevSpanY; 458 } 459 460 /** 461 * Return the scaling factor from the previous scale event to the current 462 * event. This value is defined as 463 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 464 * 465 * @return The current scaling factor. 466 */ 467 public float getScaleFactor() { 468 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; 469 } 470 471 /** 472 * Return the time difference in milliseconds between the previous 473 * accepted scaling event and the current scaling event. 474 * 475 * @return Time difference since the last scaling event in milliseconds. 476 */ 477 public long getTimeDelta() { 478 return mCurrTime - mPrevTime; 479 } 480 481 /** 482 * Return the event time of the current event being processed. 483 * 484 * @return Current event time in milliseconds. 485 */ 486 public long getEventTime() { 487 return mCurrTime; 488 } 489} 490