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 17 package android.view; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.os.Build; 22 import android.os.Handler; 23 import android.os.SystemClock; 24 import android.util.FloatMath; 25 26 /** 27 * Detects scaling transformation gestures using the supplied {@link MotionEvent}s. 28 * The {@link OnScaleGestureListener} callback will notify users when a particular 29 * gesture event has occurred. 30 * 31 * This class should only be used with {@link MotionEvent}s reported via touch. 32 * 33 * To use this class: 34 * <ul> 35 * <li>Create an instance of the {@code ScaleGestureDetector} for your 36 * {@link View} 37 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 38 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 39 * callback will be executed when the events occur. 40 * </ul> 41 */ 42 public class ScaleGestureDetector { 43 private static final String TAG = "ScaleGestureDetector"; 44 45 /** 46 * The listener for receiving notifications when gestures occur. 47 * If you want to listen for all the different gestures then implement 48 * this interface. If you only want to listen for a subset it might 49 * be easier to extend {@link SimpleOnScaleGestureListener}. 50 * 51 * An application will receive events in the following order: 52 * <ul> 53 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 54 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 55 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 56 * </ul> 57 */ 58 public interface OnScaleGestureListener { 59 /** 60 * Responds to scaling events for a gesture in progress. 61 * Reported by pointer motion. 62 * 63 * @param detector The detector reporting the event - use this to 64 * retrieve extended info about event state. 65 * @return Whether or not the detector should consider this event 66 * as handled. If an event was not handled, the detector 67 * will continue to accumulate movement until an event is 68 * handled. This can be useful if an application, for example, 69 * only wants to update scaling factors if the change is 70 * greater than 0.01. 71 */ onScale(ScaleGestureDetector detector)72 public boolean onScale(ScaleGestureDetector detector); 73 74 /** 75 * Responds to the beginning of a scaling gesture. Reported by 76 * new pointers going down. 77 * 78 * @param detector The detector reporting the event - use this to 79 * retrieve extended info about event state. 80 * @return Whether or not the detector should continue recognizing 81 * this gesture. For example, if a gesture is beginning 82 * with a focal point outside of a region where it makes 83 * sense, onScaleBegin() may return false to ignore the 84 * rest of the gesture. 85 */ onScaleBegin(ScaleGestureDetector detector)86 public boolean onScaleBegin(ScaleGestureDetector detector); 87 88 /** 89 * Responds to the end of a scale gesture. Reported by existing 90 * pointers going up. 91 * 92 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 93 * and {@link ScaleGestureDetector#getFocusY()} will return focal point 94 * of the pointers remaining on the screen. 95 * 96 * @param detector The detector reporting the event - use this to 97 * retrieve extended info about event state. 98 */ onScaleEnd(ScaleGestureDetector detector)99 public void onScaleEnd(ScaleGestureDetector detector); 100 } 101 102 /** 103 * A convenience class to extend when you only want to listen for a subset 104 * of scaling-related events. This implements all methods in 105 * {@link OnScaleGestureListener} but does nothing. 106 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns 107 * {@code false} so that a subclass can retrieve the accumulated scale 108 * factor in an overridden onScaleEnd. 109 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns 110 * {@code true}. 111 */ 112 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { 113 onScale(ScaleGestureDetector detector)114 public boolean onScale(ScaleGestureDetector detector) { 115 return false; 116 } 117 onScaleBegin(ScaleGestureDetector detector)118 public boolean onScaleBegin(ScaleGestureDetector detector) { 119 return true; 120 } 121 onScaleEnd(ScaleGestureDetector detector)122 public void onScaleEnd(ScaleGestureDetector detector) { 123 // Intentionally empty 124 } 125 } 126 127 private final Context mContext; 128 private final OnScaleGestureListener mListener; 129 130 private float mFocusX; 131 private float mFocusY; 132 133 private boolean mQuickScaleEnabled; 134 135 private float mCurrSpan; 136 private float mPrevSpan; 137 private float mInitialSpan; 138 private float mCurrSpanX; 139 private float mCurrSpanY; 140 private float mPrevSpanX; 141 private float mPrevSpanY; 142 private long mCurrTime; 143 private long mPrevTime; 144 private boolean mInProgress; 145 private int mSpanSlop; 146 private int mMinSpan; 147 148 // Bounds for recently seen values 149 private float mTouchUpper; 150 private float mTouchLower; 151 private float mTouchHistoryLastAccepted; 152 private int mTouchHistoryDirection; 153 private long mTouchHistoryLastAcceptedTime; 154 private int mTouchMinMajor; 155 private MotionEvent mDoubleTapEvent; 156 private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 157 private final Handler mHandler; 158 159 private static final long TOUCH_STABILIZE_TIME = 128; // ms 160 private static final int DOUBLE_TAP_MODE_NONE = 0; 161 private static final int DOUBLE_TAP_MODE_IN_PROGRESS = 1; 162 private static final float SCALE_FACTOR = .5f; 163 164 165 /** 166 * Consistency verifier for debugging purposes. 167 */ 168 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = 169 InputEventConsistencyVerifier.isInstrumentationEnabled() ? 170 new InputEventConsistencyVerifier(this, 0) : null; 171 private GestureDetector mGestureDetector; 172 173 private boolean mEventBeforeOrAboveStartingGestureEvent; 174 175 /** 176 * Creates a ScaleGestureDetector with the supplied listener. 177 * You may only use this constructor from a {@link android.os.Looper Looper} thread. 178 * 179 * @param context the application's context 180 * @param listener the listener invoked for all the callbacks, this must 181 * not be null. 182 * 183 * @throws NullPointerException if {@code listener} is null. 184 */ ScaleGestureDetector(Context context, OnScaleGestureListener listener)185 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 186 this(context, listener, null); 187 } 188 189 /** 190 * Creates a ScaleGestureDetector with the supplied listener. 191 * @see android.os.Handler#Handler() 192 * 193 * @param context the application's context 194 * @param listener the listener invoked for all the callbacks, this must 195 * not be null. 196 * @param handler the handler to use for running deferred listener events. 197 * 198 * @throws NullPointerException if {@code listener} is null. 199 */ ScaleGestureDetector(Context context, OnScaleGestureListener listener, Handler handler)200 public ScaleGestureDetector(Context context, OnScaleGestureListener listener, 201 Handler handler) { 202 mContext = context; 203 mListener = listener; 204 mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; 205 206 final Resources res = context.getResources(); 207 mTouchMinMajor = res.getDimensionPixelSize( 208 com.android.internal.R.dimen.config_minScalingTouchMajor); 209 mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan); 210 mHandler = handler; 211 // Quick scale is enabled by default after JB_MR2 212 if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) { 213 setQuickScaleEnabled(true); 214 } 215 } 216 217 /** 218 * The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on 219 * some hardware/driver combos. Smooth it out to get kinder, gentler behavior. 220 * @param ev MotionEvent to add to the ongoing history 221 */ addTouchHistory(MotionEvent ev)222 private void addTouchHistory(MotionEvent ev) { 223 final long currentTime = SystemClock.uptimeMillis(); 224 final int count = ev.getPointerCount(); 225 boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME; 226 float total = 0; 227 int sampleCount = 0; 228 for (int i = 0; i < count; i++) { 229 final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted); 230 final int historySize = ev.getHistorySize(); 231 final int pointerSampleCount = historySize + 1; 232 for (int h = 0; h < pointerSampleCount; h++) { 233 float major; 234 if (h < historySize) { 235 major = ev.getHistoricalTouchMajor(i, h); 236 } else { 237 major = ev.getTouchMajor(i); 238 } 239 if (major < mTouchMinMajor) major = mTouchMinMajor; 240 total += major; 241 242 if (Float.isNaN(mTouchUpper) || major > mTouchUpper) { 243 mTouchUpper = major; 244 } 245 if (Float.isNaN(mTouchLower) || major < mTouchLower) { 246 mTouchLower = major; 247 } 248 249 if (hasLastAccepted) { 250 final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted); 251 if (directionSig != mTouchHistoryDirection || 252 (directionSig == 0 && mTouchHistoryDirection == 0)) { 253 mTouchHistoryDirection = directionSig; 254 final long time = h < historySize ? ev.getHistoricalEventTime(h) 255 : ev.getEventTime(); 256 mTouchHistoryLastAcceptedTime = time; 257 accept = false; 258 } 259 } 260 } 261 sampleCount += pointerSampleCount; 262 } 263 264 final float avg = total / sampleCount; 265 266 if (accept) { 267 float newAccepted = (mTouchUpper + mTouchLower + avg) / 3; 268 mTouchUpper = (mTouchUpper + newAccepted) / 2; 269 mTouchLower = (mTouchLower + newAccepted) / 2; 270 mTouchHistoryLastAccepted = newAccepted; 271 mTouchHistoryDirection = 0; 272 mTouchHistoryLastAcceptedTime = ev.getEventTime(); 273 } 274 } 275 276 /** 277 * Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP. 278 * @see #addTouchHistory(MotionEvent) 279 */ 280 private void clearTouchHistory() { 281 mTouchUpper = Float.NaN; 282 mTouchLower = Float.NaN; 283 mTouchHistoryLastAccepted = Float.NaN; 284 mTouchHistoryDirection = 0; 285 mTouchHistoryLastAcceptedTime = 0; 286 } 287 288 /** 289 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} 290 * when appropriate. 291 * 292 * <p>Applications should pass a complete and consistent event stream to this method. 293 * A complete and consistent event stream involves all MotionEvents from the initial 294 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> 295 * 296 * @param event The event to process 297 * @return true if the event was processed and the detector wants to receive the 298 * rest of the MotionEvents in this event stream. 299 */ 300 public boolean onTouchEvent(MotionEvent event) { 301 if (mInputEventConsistencyVerifier != null) { 302 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 303 } 304 305 mCurrTime = event.getEventTime(); 306 307 final int action = event.getActionMasked(); 308 309 // Forward the event to check for double tap gesture 310 if (mQuickScaleEnabled) { 311 mGestureDetector.onTouchEvent(event); 312 } 313 314 final boolean streamComplete = action == MotionEvent.ACTION_UP || 315 action == MotionEvent.ACTION_CANCEL; 316 317 if (action == MotionEvent.ACTION_DOWN || streamComplete) { 318 // Reset any scale in progress with the listener. 319 // If it's an ACTION_DOWN we're beginning a new event stream. 320 // This means the app probably didn't give us all the events. Shame on it. 321 if (mInProgress) { 322 mListener.onScaleEnd(this); 323 mInProgress = false; 324 mInitialSpan = 0; 325 mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 326 } else if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS && streamComplete) { 327 mInProgress = false; 328 mInitialSpan = 0; 329 mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 330 } 331 332 if (streamComplete) { 333 clearTouchHistory(); 334 return true; 335 } 336 } 337 338 final boolean configChanged = action == MotionEvent.ACTION_DOWN || 339 action == MotionEvent.ACTION_POINTER_UP || 340 action == MotionEvent.ACTION_POINTER_DOWN; 341 342 343 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; 344 final int skipIndex = pointerUp ? event.getActionIndex() : -1; 345 346 // Determine focal point 347 float sumX = 0, sumY = 0; 348 final int count = event.getPointerCount(); 349 final int div = pointerUp ? count - 1 : count; 350 final float focusX; 351 final float focusY; 352 if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) { 353 // In double tap mode, the focal pt is always where the double tap 354 // gesture started 355 focusX = mDoubleTapEvent.getX(); 356 focusY = mDoubleTapEvent.getY(); 357 if (event.getY() < focusY) { 358 mEventBeforeOrAboveStartingGestureEvent = true; 359 } else { 360 mEventBeforeOrAboveStartingGestureEvent = false; 361 } 362 } else { 363 for (int i = 0; i < count; i++) { 364 if (skipIndex == i) continue; 365 sumX += event.getX(i); 366 sumY += event.getY(i); 367 } 368 369 focusX = sumX / div; 370 focusY = sumY / div; 371 } 372 373 addTouchHistory(event); 374 375 // Determine average deviation from focal point 376 float devSumX = 0, devSumY = 0; 377 for (int i = 0; i < count; i++) { 378 if (skipIndex == i) continue; 379 380 // Convert the resulting diameter into a radius. 381 final float touchSize = mTouchHistoryLastAccepted / 2; 382 devSumX += Math.abs(event.getX(i) - focusX) + touchSize; 383 devSumY += Math.abs(event.getY(i) - focusY) + touchSize; 384 } 385 final float devX = devSumX / div; 386 final float devY = devSumY / div; 387 388 // Span is the average distance between touch points through the focal point; 389 // i.e. the diameter of the circle with a radius of the average deviation from 390 // the focal point. 391 final float spanX = devX * 2; 392 final float spanY = devY * 2; 393 final float span; 394 if (inDoubleTapMode()) { 395 span = spanY; 396 } else { 397 span = FloatMath.sqrt(spanX * spanX + spanY * spanY); 398 } 399 400 // Dispatch begin/end events as needed. 401 // If the configuration changes, notify the app to reset its current state by beginning 402 // a fresh scale event stream. 403 final boolean wasInProgress = mInProgress; 404 mFocusX = focusX; 405 mFocusY = focusY; 406 if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) { 407 mListener.onScaleEnd(this); 408 mInProgress = false; 409 mInitialSpan = span; 410 mDoubleTapMode = DOUBLE_TAP_MODE_NONE; 411 } 412 if (configChanged) { 413 mPrevSpanX = mCurrSpanX = spanX; 414 mPrevSpanY = mCurrSpanY = spanY; 415 mInitialSpan = mPrevSpan = mCurrSpan = span; 416 } 417 418 final int minSpan = inDoubleTapMode() ? mSpanSlop : mMinSpan; 419 if (!mInProgress && span >= minSpan && 420 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) { 421 mPrevSpanX = mCurrSpanX = spanX; 422 mPrevSpanY = mCurrSpanY = spanY; 423 mPrevSpan = mCurrSpan = span; 424 mPrevTime = mCurrTime; 425 mInProgress = mListener.onScaleBegin(this); 426 } 427 428 // Handle motion; focal point and span/scale factor are changing. 429 if (action == MotionEvent.ACTION_MOVE) { 430 mCurrSpanX = spanX; 431 mCurrSpanY = spanY; 432 mCurrSpan = span; 433 434 boolean updatePrev = true; 435 436 if (mInProgress) { 437 updatePrev = mListener.onScale(this); 438 } 439 440 if (updatePrev) { 441 mPrevSpanX = mCurrSpanX; 442 mPrevSpanY = mCurrSpanY; 443 mPrevSpan = mCurrSpan; 444 mPrevTime = mCurrTime; 445 } 446 } 447 448 return true; 449 } 450 451 452 private boolean inDoubleTapMode() { 453 return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS; 454 } 455 456 /** 457 * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks 458 * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default 459 * if the app targets API 19 and newer. 460 * @param scales true to enable quick scaling, false to disable 461 */ 462 public void setQuickScaleEnabled(boolean scales) { 463 mQuickScaleEnabled = scales; 464 if (mQuickScaleEnabled && mGestureDetector == null) { 465 GestureDetector.SimpleOnGestureListener gestureListener = 466 new GestureDetector.SimpleOnGestureListener() { 467 @Override 468 public boolean onDoubleTap(MotionEvent e) { 469 // Double tap: start watching for a swipe 470 mDoubleTapEvent = e; 471 mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS; 472 return true; 473 } 474 }; 475 mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler); 476 } 477 } 478 479 /** 480 * Return whether the quick scale gesture, in which the user performs a double tap followed by a 481 * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}. 482 */ 483 public boolean isQuickScaleEnabled() { 484 return mQuickScaleEnabled; 485 } 486 487 /** 488 * Returns {@code true} if a scale gesture is in progress. 489 */ 490 public boolean isInProgress() { 491 return mInProgress; 492 } 493 494 /** 495 * Get the X coordinate of the current gesture's focal point. 496 * If a gesture is in progress, the focal point is between 497 * each of the pointers forming the gesture. 498 * 499 * If {@link #isInProgress()} would return false, the result of this 500 * function is undefined. 501 * 502 * @return X coordinate of the focal point in pixels. 503 */ 504 public float getFocusX() { 505 return mFocusX; 506 } 507 508 /** 509 * Get the Y coordinate of the current gesture's focal point. 510 * If a gesture is in progress, the focal point is between 511 * each of the pointers forming the gesture. 512 * 513 * If {@link #isInProgress()} would return false, the result of this 514 * function is undefined. 515 * 516 * @return Y coordinate of the focal point in pixels. 517 */ 518 public float getFocusY() { 519 return mFocusY; 520 } 521 522 /** 523 * Return the average distance between each of the pointers forming the 524 * gesture in progress through the focal point. 525 * 526 * @return Distance between pointers in pixels. 527 */ 528 public float getCurrentSpan() { 529 return mCurrSpan; 530 } 531 532 /** 533 * Return the average X distance between each of the pointers forming the 534 * gesture in progress through the focal point. 535 * 536 * @return Distance between pointers in pixels. 537 */ 538 public float getCurrentSpanX() { 539 return mCurrSpanX; 540 } 541 542 /** 543 * Return the average Y distance between each of the pointers forming the 544 * gesture in progress through the focal point. 545 * 546 * @return Distance between pointers in pixels. 547 */ 548 public float getCurrentSpanY() { 549 return mCurrSpanY; 550 } 551 552 /** 553 * Return the previous average distance between each of the pointers forming the 554 * gesture in progress through the focal point. 555 * 556 * @return Previous distance between pointers in pixels. 557 */ 558 public float getPreviousSpan() { 559 return mPrevSpan; 560 } 561 562 /** 563 * Return the previous average X distance between each of the pointers forming the 564 * gesture in progress through the focal point. 565 * 566 * @return Previous distance between pointers in pixels. 567 */ 568 public float getPreviousSpanX() { 569 return mPrevSpanX; 570 } 571 572 /** 573 * Return the previous average Y distance between each of the pointers forming the 574 * gesture in progress through the focal point. 575 * 576 * @return Previous distance between pointers in pixels. 577 */ 578 public float getPreviousSpanY() { 579 return mPrevSpanY; 580 } 581 582 /** 583 * Return the scaling factor from the previous scale event to the current 584 * event. This value is defined as 585 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 586 * 587 * @return The current scaling factor. 588 */ 589 public float getScaleFactor() { 590 if (inDoubleTapMode()) { 591 // Drag is moving up; the further away from the gesture 592 // start, the smaller the span should be, the closer, 593 // the larger the span, and therefore the larger the scale 594 final boolean scaleUp = 595 (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) || 596 (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan)); 597 final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR); 598 return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff); 599 } 600 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; 601 } 602 603 /** 604 * Return the time difference in milliseconds between the previous 605 * accepted scaling event and the current scaling event. 606 * 607 * @return Time difference since the last scaling event in milliseconds. 608 */ 609 public long getTimeDelta() { 610 return mCurrTime - mPrevTime; 611 } 612 613 /** 614 * Return the event time of the current event being processed. 615 * 616 * @return Current event time in milliseconds. 617 */ 618 public long getEventTime() { 619 return mCurrTime; 620 } 621 }