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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.Context; 23 import android.os.Build; 24 import android.os.Handler; 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(@onNull ScaleGestureDetector detector)72 public boolean onScale(@NonNull 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(@onNull ScaleGestureDetector detector)86 public boolean onScaleBegin(@NonNull 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(@onNull ScaleGestureDetector detector)99 public void onScaleEnd(@NonNull 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(@onNull ScaleGestureDetector detector)114 public boolean onScale(@NonNull ScaleGestureDetector detector) { 115 return false; 116 } 117 onScaleBegin(@onNull ScaleGestureDetector detector)118 public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) { 119 return true; 120 } 121 onScaleEnd(@onNull ScaleGestureDetector detector)122 public void onScaleEnd(@NonNull ScaleGestureDetector detector) { 123 // Intentionally empty 124 } 125 } 126 127 private final Context mContext; 128 @UnsupportedAppUsage 129 private final OnScaleGestureListener mListener; 130 131 private float mFocusX; 132 private float mFocusY; 133 134 private boolean mQuickScaleEnabled; 135 private boolean mStylusScaleEnabled; 136 137 private float mCurrSpan; 138 private float mPrevSpan; 139 private float mInitialSpan; 140 private float mCurrSpanX; 141 private float mCurrSpanY; 142 private float mPrevSpanX; 143 private float mPrevSpanY; 144 private long mCurrTime; 145 private long mPrevTime; 146 private boolean mInProgress; 147 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938) 148 private int mSpanSlop; 149 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768938) 150 private int mMinSpan; 151 152 private final Handler mHandler; 153 154 private float mAnchoredScaleStartX; 155 private float mAnchoredScaleStartY; 156 private int mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE; 157 158 private static final long TOUCH_STABILIZE_TIME = 128; // ms 159 private static final float SCALE_FACTOR = .5f; 160 private static final int ANCHORED_SCALE_MODE_NONE = 0; 161 private static final int ANCHORED_SCALE_MODE_DOUBLE_TAP = 1; 162 private static final int ANCHORED_SCALE_MODE_STYLUS = 2; 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(@onNull Context context, @NonNull OnScaleGestureListener listener)185 public ScaleGestureDetector(@NonNull Context context, 186 @NonNull OnScaleGestureListener listener) { 187 this(context, listener, null); 188 } 189 190 /** 191 * Creates a ScaleGestureDetector with the supplied listener. 192 * @see android.os.Handler#Handler() 193 * 194 * @param context the application's context 195 * @param listener the listener invoked for all the callbacks, this must 196 * not be null. 197 * @param handler the handler to use for running deferred listener events. 198 * 199 * @throws NullPointerException if {@code listener} is null. 200 */ ScaleGestureDetector(@onNull Context context, @NonNull OnScaleGestureListener listener, @Nullable Handler handler)201 public ScaleGestureDetector(@NonNull Context context, @NonNull OnScaleGestureListener listener, 202 @Nullable Handler handler) { 203 this(context, ViewConfiguration.get(context).getScaledTouchSlop() * 2, 204 ViewConfiguration.get(context).getScaledMinimumScalingSpan(), handler, listener); 205 } 206 207 /** 208 * Creates a ScaleGestureDetector with span slop and min span. 209 * 210 * @param context the application's context. 211 * @param spanSlop the threshold for interpreting a touch movement as scaling. 212 * @param minSpan the minimum threshold of scaling span. The span could be 213 * overridden by other usages to specify a different scaling span, for instance, 214 * if you need pinch gestures to continue closer together than the default. 215 * @param listener the listener invoked for all the callbacks, this must not be null. 216 * @param handler the handler to use for running deferred listener events. 217 * 218 * @throws NullPointerException if {@code listener} is null. 219 * 220 * @hide 221 */ ScaleGestureDetector(@onNull Context context, @NonNull int spanSlop, @NonNull int minSpan, @Nullable Handler handler, @NonNull OnScaleGestureListener listener)222 public ScaleGestureDetector(@NonNull Context context, @NonNull int spanSlop, 223 @NonNull int minSpan, @Nullable Handler handler, 224 @NonNull OnScaleGestureListener listener) { 225 mContext = context; 226 mListener = listener; 227 mSpanSlop = spanSlop; 228 mMinSpan = minSpan; 229 mHandler = handler; 230 // Quick scale is enabled by default after JB_MR2 231 final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion; 232 if (targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) { 233 setQuickScaleEnabled(true); 234 } 235 // Stylus scale is enabled by default after LOLLIPOP_MR1 236 if (targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) { 237 setStylusScaleEnabled(true); 238 } 239 } 240 241 /** 242 * Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} 243 * when appropriate. 244 * 245 * <p>Applications should pass a complete and consistent event stream to this method. 246 * A complete and consistent event stream involves all MotionEvents from the initial 247 * ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.</p> 248 * 249 * @param event The event to process 250 * @return true if the event was processed and the detector wants to receive the 251 * rest of the MotionEvents in this event stream. 252 */ onTouchEvent(@onNull MotionEvent event)253 public boolean onTouchEvent(@NonNull MotionEvent event) { 254 if (mInputEventConsistencyVerifier != null) { 255 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 256 } 257 258 mCurrTime = event.getEventTime(); 259 260 final int action = event.getActionMasked(); 261 262 // Forward the event to check for double tap gesture 263 if (mQuickScaleEnabled) { 264 mGestureDetector.onTouchEvent(event); 265 } 266 267 final int count = event.getPointerCount(); 268 final boolean isStylusButtonDown = 269 (event.getButtonState() & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0; 270 271 final boolean anchoredScaleCancelled = 272 mAnchoredScaleMode == ANCHORED_SCALE_MODE_STYLUS && !isStylusButtonDown; 273 final boolean streamComplete = action == MotionEvent.ACTION_UP || 274 action == MotionEvent.ACTION_CANCEL || anchoredScaleCancelled; 275 276 if (action == MotionEvent.ACTION_DOWN || streamComplete) { 277 // Reset any scale in progress with the listener. 278 // If it's an ACTION_DOWN we're beginning a new event stream. 279 // This means the app probably didn't give us all the events. Shame on it. 280 if (mInProgress) { 281 mListener.onScaleEnd(this); 282 mInProgress = false; 283 mInitialSpan = 0; 284 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE; 285 } else if (inAnchoredScaleMode() && streamComplete) { 286 mInProgress = false; 287 mInitialSpan = 0; 288 mAnchoredScaleMode = ANCHORED_SCALE_MODE_NONE; 289 } 290 291 if (streamComplete) { 292 return true; 293 } 294 } 295 296 if (!mInProgress && mStylusScaleEnabled && !inAnchoredScaleMode() 297 && !streamComplete && isStylusButtonDown) { 298 // Start of a button scale gesture 299 mAnchoredScaleStartX = event.getX(); 300 mAnchoredScaleStartY = event.getY(); 301 mAnchoredScaleMode = ANCHORED_SCALE_MODE_STYLUS; 302 mInitialSpan = 0; 303 } 304 305 final boolean configChanged = action == MotionEvent.ACTION_DOWN || 306 action == MotionEvent.ACTION_POINTER_UP || 307 action == MotionEvent.ACTION_POINTER_DOWN || anchoredScaleCancelled; 308 309 final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; 310 final int skipIndex = pointerUp ? event.getActionIndex() : -1; 311 312 // Determine focal point 313 float sumX = 0, sumY = 0; 314 final int div = pointerUp ? count - 1 : count; 315 final float focusX; 316 final float focusY; 317 if (inAnchoredScaleMode()) { 318 // In anchored scale mode, the focal pt is always where the double tap 319 // or button down gesture started 320 focusX = mAnchoredScaleStartX; 321 focusY = mAnchoredScaleStartY; 322 if (event.getY() < focusY) { 323 mEventBeforeOrAboveStartingGestureEvent = true; 324 } else { 325 mEventBeforeOrAboveStartingGestureEvent = false; 326 } 327 } else { 328 for (int i = 0; i < count; i++) { 329 if (skipIndex == i) continue; 330 sumX += event.getX(i); 331 sumY += event.getY(i); 332 } 333 334 focusX = sumX / div; 335 focusY = sumY / div; 336 } 337 338 // Determine average deviation from focal point 339 float devSumX = 0, devSumY = 0; 340 for (int i = 0; i < count; i++) { 341 if (skipIndex == i) continue; 342 343 // Convert the resulting diameter into a radius. 344 devSumX += Math.abs(event.getX(i) - focusX); 345 devSumY += Math.abs(event.getY(i) - focusY); 346 } 347 final float devX = devSumX / div; 348 final float devY = devSumY / div; 349 350 // Span is the average distance between touch points through the focal point; 351 // i.e. the diameter of the circle with a radius of the average deviation from 352 // the focal point. 353 final float spanX = devX * 2; 354 final float spanY = devY * 2; 355 final float span; 356 if (inAnchoredScaleMode()) { 357 span = spanY; 358 } else { 359 span = (float) Math.hypot(spanX, spanY); 360 } 361 362 // Dispatch begin/end events as needed. 363 // If the configuration changes, notify the app to reset its current state by beginning 364 // a fresh scale event stream. 365 final boolean wasInProgress = mInProgress; 366 mFocusX = focusX; 367 mFocusY = focusY; 368 if (!inAnchoredScaleMode() && mInProgress && (span < mMinSpan || configChanged)) { 369 mListener.onScaleEnd(this); 370 mInProgress = false; 371 mInitialSpan = span; 372 } 373 if (configChanged) { 374 mPrevSpanX = mCurrSpanX = spanX; 375 mPrevSpanY = mCurrSpanY = spanY; 376 mInitialSpan = mPrevSpan = mCurrSpan = span; 377 } 378 379 final int minSpan = inAnchoredScaleMode() ? mSpanSlop : mMinSpan; 380 if (!mInProgress && span >= minSpan && 381 (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) { 382 mPrevSpanX = mCurrSpanX = spanX; 383 mPrevSpanY = mCurrSpanY = spanY; 384 mPrevSpan = mCurrSpan = span; 385 mPrevTime = mCurrTime; 386 mInProgress = mListener.onScaleBegin(this); 387 } 388 389 // Handle motion; focal point and span/scale factor are changing. 390 if (action == MotionEvent.ACTION_MOVE) { 391 mCurrSpanX = spanX; 392 mCurrSpanY = spanY; 393 mCurrSpan = span; 394 395 boolean updatePrev = true; 396 397 if (mInProgress) { 398 updatePrev = mListener.onScale(this); 399 } 400 401 if (updatePrev) { 402 mPrevSpanX = mCurrSpanX; 403 mPrevSpanY = mCurrSpanY; 404 mPrevSpan = mCurrSpan; 405 mPrevTime = mCurrTime; 406 } 407 } 408 409 return true; 410 } 411 inAnchoredScaleMode()412 private boolean inAnchoredScaleMode() { 413 return mAnchoredScaleMode != ANCHORED_SCALE_MODE_NONE; 414 } 415 416 /** 417 * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks 418 * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default 419 * if the app targets API 19 and newer. 420 * @param scales true to enable quick scaling, false to disable 421 */ setQuickScaleEnabled(boolean scales)422 public void setQuickScaleEnabled(boolean scales) { 423 mQuickScaleEnabled = scales; 424 if (mQuickScaleEnabled && mGestureDetector == null) { 425 GestureDetector.SimpleOnGestureListener gestureListener = 426 new GestureDetector.SimpleOnGestureListener() { 427 @Override 428 public boolean onDoubleTap(MotionEvent e) { 429 // Double tap: start watching for a swipe 430 mAnchoredScaleStartX = e.getX(); 431 mAnchoredScaleStartY = e.getY(); 432 mAnchoredScaleMode = ANCHORED_SCALE_MODE_DOUBLE_TAP; 433 return true; 434 } 435 }; 436 mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler); 437 } 438 } 439 440 /** 441 * Return whether the quick scale gesture, in which the user performs a double tap followed by a 442 * swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}. 443 */ isQuickScaleEnabled()444 public boolean isQuickScaleEnabled() { 445 return mQuickScaleEnabled; 446 } 447 448 /** 449 * Sets whether the associates {@link OnScaleGestureListener} should receive 450 * onScale callbacks when the user uses a stylus and presses the button. 451 * Note that this is enabled by default if the app targets API 23 and newer. 452 * 453 * @param scales true to enable stylus scaling, false to disable. 454 */ setStylusScaleEnabled(boolean scales)455 public void setStylusScaleEnabled(boolean scales) { 456 mStylusScaleEnabled = scales; 457 } 458 459 /** 460 * Return whether the stylus scale gesture, in which the user uses a stylus and presses the 461 * button, should perform scaling. {@see #setStylusScaleEnabled(boolean)} 462 */ isStylusScaleEnabled()463 public boolean isStylusScaleEnabled() { 464 return mStylusScaleEnabled; 465 } 466 467 /** 468 * Returns {@code true} if a scale gesture is in progress. 469 */ isInProgress()470 public boolean isInProgress() { 471 return mInProgress; 472 } 473 474 /** 475 * Get the X coordinate of the current gesture's focal point. 476 * If a gesture is in progress, the focal point is between 477 * each of the pointers forming the gesture. 478 * 479 * If {@link #isInProgress()} would return false, the result of this 480 * function is undefined. 481 * 482 * @return X coordinate of the focal point in pixels. 483 */ getFocusX()484 public float getFocusX() { 485 return mFocusX; 486 } 487 488 /** 489 * Get the Y coordinate of the current gesture's focal point. 490 * If a gesture is in progress, the focal point is between 491 * each of the pointers forming the gesture. 492 * 493 * If {@link #isInProgress()} would return false, the result of this 494 * function is undefined. 495 * 496 * @return Y coordinate of the focal point in pixels. 497 */ getFocusY()498 public float getFocusY() { 499 return mFocusY; 500 } 501 502 /** 503 * Return the average distance between each of the pointers forming the 504 * gesture in progress through the focal point. 505 * 506 * @return Distance between pointers in pixels. 507 */ getCurrentSpan()508 public float getCurrentSpan() { 509 return mCurrSpan; 510 } 511 512 /** 513 * Return the average X distance between each of the pointers forming the 514 * gesture in progress through the focal point. 515 * 516 * @return Distance between pointers in pixels. 517 */ getCurrentSpanX()518 public float getCurrentSpanX() { 519 return mCurrSpanX; 520 } 521 522 /** 523 * Return the average Y 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 */ getCurrentSpanY()528 public float getCurrentSpanY() { 529 return mCurrSpanY; 530 } 531 532 /** 533 * Return the previous average distance between each of the pointers forming the 534 * gesture in progress through the focal point. 535 * 536 * @return Previous distance between pointers in pixels. 537 */ getPreviousSpan()538 public float getPreviousSpan() { 539 return mPrevSpan; 540 } 541 542 /** 543 * Return the previous average X distance between each of the pointers forming the 544 * gesture in progress through the focal point. 545 * 546 * @return Previous distance between pointers in pixels. 547 */ getPreviousSpanX()548 public float getPreviousSpanX() { 549 return mPrevSpanX; 550 } 551 552 /** 553 * Return the previous average Y 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 */ getPreviousSpanY()558 public float getPreviousSpanY() { 559 return mPrevSpanY; 560 } 561 562 /** 563 * Return the scaling factor from the previous scale event to the current 564 * event. This value is defined as 565 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 566 * 567 * @return The current scaling factor. 568 */ getScaleFactor()569 public float getScaleFactor() { 570 if (inAnchoredScaleMode()) { 571 // Drag is moving up; the further away from the gesture 572 // start, the smaller the span should be, the closer, 573 // the larger the span, and therefore the larger the scale 574 final boolean scaleUp = 575 (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) || 576 (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan)); 577 final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR); 578 return mPrevSpan <= mSpanSlop ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff); 579 } 580 return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; 581 } 582 583 /** 584 * Return the time difference in milliseconds between the previous 585 * accepted scaling event and the current scaling event. 586 * 587 * @return Time difference since the last scaling event in milliseconds. 588 */ getTimeDelta()589 public long getTimeDelta() { 590 return mCurrTime - mPrevTime; 591 } 592 593 /** 594 * Return the event time of the current event being processed. 595 * 596 * @return Current event time in milliseconds. 597 */ getEventTime()598 public long getEventTime() { 599 return mCurrTime; 600 } 601 }