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