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