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