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.util.DisplayMetrics; 21 import android.util.FloatMath; 22 import android.util.Log; 23 24 /** 25 * Detects transformation gestures involving more than one pointer ("multitouch") 26 * using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} 27 * callback will notify users when a particular gesture event has occurred. 28 * This class should only be used with {@link MotionEvent}s reported via touch. 29 * 30 * To use this class: 31 * <ul> 32 * <li>Create an instance of the {@code ScaleGestureDetector} for your 33 * {@link View} 34 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 35 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your 36 * callback will be executed when the events occur. 37 * </ul> 38 */ 39 public class ScaleGestureDetector { 40 private static final String TAG = "ScaleGestureDetector"; 41 42 /** 43 * The listener for receiving notifications when gestures occur. 44 * If you want to listen for all the different gestures then implement 45 * this interface. If you only want to listen for a subset it might 46 * be easier to extend {@link SimpleOnScaleGestureListener}. 47 * 48 * An application will receive events in the following order: 49 * <ul> 50 * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} 51 * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} 52 * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)} 53 * </ul> 54 */ 55 public interface OnScaleGestureListener { 56 /** 57 * Responds to scaling events for a gesture in progress. 58 * Reported by pointer motion. 59 * 60 * @param detector The detector reporting the event - use this to 61 * retrieve extended info about event state. 62 * @return Whether or not the detector should consider this event 63 * as handled. If an event was not handled, the detector 64 * will continue to accumulate movement until an event is 65 * handled. This can be useful if an application, for example, 66 * only wants to update scaling factors if the change is 67 * greater than 0.01. 68 */ onScale(ScaleGestureDetector detector)69 public boolean onScale(ScaleGestureDetector detector); 70 71 /** 72 * Responds to the beginning of a scaling gesture. Reported by 73 * new pointers going down. 74 * 75 * @param detector The detector reporting the event - use this to 76 * retrieve extended info about event state. 77 * @return Whether or not the detector should continue recognizing 78 * this gesture. For example, if a gesture is beginning 79 * with a focal point outside of a region where it makes 80 * sense, onScaleBegin() may return false to ignore the 81 * rest of the gesture. 82 */ onScaleBegin(ScaleGestureDetector detector)83 public boolean onScaleBegin(ScaleGestureDetector detector); 84 85 /** 86 * Responds to the end of a scale gesture. Reported by existing 87 * pointers going up. 88 * 89 * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()} 90 * and {@link ScaleGestureDetector#getFocusY()} will return the location 91 * of the pointer remaining on the screen. 92 * 93 * @param detector The detector reporting the event - use this to 94 * retrieve extended info about event state. 95 */ onScaleEnd(ScaleGestureDetector detector)96 public void onScaleEnd(ScaleGestureDetector detector); 97 } 98 99 /** 100 * A convenience class to extend when you only want to listen for a subset 101 * of scaling-related events. This implements all methods in 102 * {@link OnScaleGestureListener} but does nothing. 103 * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns 104 * {@code false} so that a subclass can retrieve the accumulated scale 105 * factor in an overridden onScaleEnd. 106 * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns 107 * {@code true}. 108 */ 109 public static class SimpleOnScaleGestureListener implements OnScaleGestureListener { 110 onScale(ScaleGestureDetector detector)111 public boolean onScale(ScaleGestureDetector detector) { 112 return false; 113 } 114 onScaleBegin(ScaleGestureDetector detector)115 public boolean onScaleBegin(ScaleGestureDetector detector) { 116 return true; 117 } 118 onScaleEnd(ScaleGestureDetector detector)119 public void onScaleEnd(ScaleGestureDetector detector) { 120 // Intentionally empty 121 } 122 } 123 124 /** 125 * This value is the threshold ratio between our previous combined pressure 126 * and the current combined pressure. We will only fire an onScale event if 127 * the computed ratio between the current and previous event pressures is 128 * greater than this value. When pressure decreases rapidly between events 129 * the position values can often be imprecise, as it usually indicates 130 * that the user is in the process of lifting a pointer off of the device. 131 * Its value was tuned experimentally. 132 */ 133 private static final float PRESSURE_THRESHOLD = 0.67f; 134 135 private final Context mContext; 136 private final OnScaleGestureListener mListener; 137 private boolean mGestureInProgress; 138 139 private MotionEvent mPrevEvent; 140 private MotionEvent mCurrEvent; 141 142 private float mFocusX; 143 private float mFocusY; 144 private float mPrevFingerDiffX; 145 private float mPrevFingerDiffY; 146 private float mCurrFingerDiffX; 147 private float mCurrFingerDiffY; 148 private float mCurrLen; 149 private float mPrevLen; 150 private float mScaleFactor; 151 private float mCurrPressure; 152 private float mPrevPressure; 153 private long mTimeDelta; 154 155 private boolean mInvalidGesture; 156 157 // Pointer IDs currently responsible for the two fingers controlling the gesture 158 private int mActiveId0; 159 private int mActiveId1; 160 private boolean mActive0MostRecent; 161 162 /** 163 * Consistency verifier for debugging purposes. 164 */ 165 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = 166 InputEventConsistencyVerifier.isInstrumentationEnabled() ? 167 new InputEventConsistencyVerifier(this, 0) : null; 168 ScaleGestureDetector(Context context, OnScaleGestureListener listener)169 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 170 mContext = context; 171 mListener = listener; 172 } 173 onTouchEvent(MotionEvent event)174 public boolean onTouchEvent(MotionEvent event) { 175 if (mInputEventConsistencyVerifier != null) { 176 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 177 } 178 179 final int action = event.getActionMasked(); 180 181 if (action == MotionEvent.ACTION_DOWN) { 182 reset(); // Start fresh 183 } 184 185 boolean handled = true; 186 if (mInvalidGesture) { 187 handled = false; 188 } else if (!mGestureInProgress) { 189 switch (action) { 190 case MotionEvent.ACTION_DOWN: { 191 mActiveId0 = event.getPointerId(0); 192 mActive0MostRecent = true; 193 } 194 break; 195 196 case MotionEvent.ACTION_UP: 197 reset(); 198 break; 199 200 case MotionEvent.ACTION_POINTER_DOWN: { 201 // We have a new multi-finger gesture 202 if (mPrevEvent != null) mPrevEvent.recycle(); 203 mPrevEvent = MotionEvent.obtain(event); 204 mTimeDelta = 0; 205 206 int index1 = event.getActionIndex(); 207 int index0 = event.findPointerIndex(mActiveId0); 208 mActiveId1 = event.getPointerId(index1); 209 if (index0 < 0 || index0 == index1) { 210 // Probably someone sending us a broken event stream. 211 index0 = findNewActiveIndex(event, mActiveId1, -1); 212 mActiveId0 = event.getPointerId(index0); 213 } 214 mActive0MostRecent = false; 215 216 setContext(event); 217 218 mGestureInProgress = mListener.onScaleBegin(this); 219 break; 220 } 221 } 222 } else { 223 // Transform gesture in progress - attempt to handle it 224 switch (action) { 225 case MotionEvent.ACTION_POINTER_DOWN: { 226 // End the old gesture and begin a new one with the most recent two fingers. 227 mListener.onScaleEnd(this); 228 final int oldActive0 = mActiveId0; 229 final int oldActive1 = mActiveId1; 230 reset(); 231 232 mPrevEvent = MotionEvent.obtain(event); 233 mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1; 234 mActiveId1 = event.getPointerId(event.getActionIndex()); 235 mActive0MostRecent = false; 236 237 int index0 = event.findPointerIndex(mActiveId0); 238 if (index0 < 0 || mActiveId0 == mActiveId1) { 239 // Probably someone sending us a broken event stream. 240 Log.e(TAG, "Got " + MotionEvent.actionToString(action) + 241 " with bad state while a gesture was in progress. " + 242 "Did you forget to pass an event to " + 243 "ScaleGestureDetector#onTouchEvent?"); 244 index0 = findNewActiveIndex(event, mActiveId1, -1); 245 mActiveId0 = event.getPointerId(index0); 246 } 247 248 setContext(event); 249 250 mGestureInProgress = mListener.onScaleBegin(this); 251 } 252 break; 253 254 case MotionEvent.ACTION_POINTER_UP: { 255 final int pointerCount = event.getPointerCount(); 256 final int actionIndex = event.getActionIndex(); 257 final int actionId = event.getPointerId(actionIndex); 258 259 boolean gestureEnded = false; 260 if (pointerCount > 2) { 261 if (actionId == mActiveId0) { 262 final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex); 263 if (newIndex >= 0) { 264 mListener.onScaleEnd(this); 265 mActiveId0 = event.getPointerId(newIndex); 266 mActive0MostRecent = true; 267 mPrevEvent = MotionEvent.obtain(event); 268 setContext(event); 269 mGestureInProgress = mListener.onScaleBegin(this); 270 } else { 271 gestureEnded = true; 272 } 273 } else if (actionId == mActiveId1) { 274 final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex); 275 if (newIndex >= 0) { 276 mListener.onScaleEnd(this); 277 mActiveId1 = event.getPointerId(newIndex); 278 mActive0MostRecent = false; 279 mPrevEvent = MotionEvent.obtain(event); 280 setContext(event); 281 mGestureInProgress = mListener.onScaleBegin(this); 282 } else { 283 gestureEnded = true; 284 } 285 } 286 mPrevEvent.recycle(); 287 mPrevEvent = MotionEvent.obtain(event); 288 setContext(event); 289 } else { 290 gestureEnded = true; 291 } 292 293 if (gestureEnded) { 294 // Gesture ended 295 setContext(event); 296 297 // Set focus point to the remaining finger 298 final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0; 299 final int index = event.findPointerIndex(activeId); 300 mFocusX = event.getX(index); 301 mFocusY = event.getY(index); 302 303 mListener.onScaleEnd(this); 304 reset(); 305 mActiveId0 = activeId; 306 mActive0MostRecent = true; 307 } 308 } 309 break; 310 311 case MotionEvent.ACTION_CANCEL: 312 mListener.onScaleEnd(this); 313 reset(); 314 break; 315 316 case MotionEvent.ACTION_UP: 317 reset(); 318 break; 319 320 case MotionEvent.ACTION_MOVE: { 321 setContext(event); 322 323 // Only accept the event if our relative pressure is within 324 // a certain limit - this can help filter shaky data as a 325 // finger is lifted. 326 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 327 final boolean updatePrevious = mListener.onScale(this); 328 329 if (updatePrevious) { 330 mPrevEvent.recycle(); 331 mPrevEvent = MotionEvent.obtain(event); 332 } 333 } 334 } 335 break; 336 } 337 } 338 339 if (!handled && mInputEventConsistencyVerifier != null) { 340 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); 341 } 342 return handled; 343 } 344 findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex)345 private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex) { 346 final int pointerCount = ev.getPointerCount(); 347 348 // It's ok if this isn't found and returns -1, it simply won't match. 349 final int otherActiveIndex = ev.findPointerIndex(otherActiveId); 350 351 // Pick a new id and update tracking state. 352 for (int i = 0; i < pointerCount; i++) { 353 if (i != removedPointerIndex && i != otherActiveIndex) { 354 return i; 355 } 356 } 357 return -1; 358 } 359 setContext(MotionEvent curr)360 private void setContext(MotionEvent curr) { 361 if (mCurrEvent != null) { 362 mCurrEvent.recycle(); 363 } 364 mCurrEvent = MotionEvent.obtain(curr); 365 366 mCurrLen = -1; 367 mPrevLen = -1; 368 mScaleFactor = -1; 369 370 final MotionEvent prev = mPrevEvent; 371 372 final int prevIndex0 = prev.findPointerIndex(mActiveId0); 373 final int prevIndex1 = prev.findPointerIndex(mActiveId1); 374 final int currIndex0 = curr.findPointerIndex(mActiveId0); 375 final int currIndex1 = curr.findPointerIndex(mActiveId1); 376 377 if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) { 378 mInvalidGesture = true; 379 Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable()); 380 if (mGestureInProgress) { 381 mListener.onScaleEnd(this); 382 } 383 return; 384 } 385 386 final float px0 = prev.getX(prevIndex0); 387 final float py0 = prev.getY(prevIndex0); 388 final float px1 = prev.getX(prevIndex1); 389 final float py1 = prev.getY(prevIndex1); 390 final float cx0 = curr.getX(currIndex0); 391 final float cy0 = curr.getY(currIndex0); 392 final float cx1 = curr.getX(currIndex1); 393 final float cy1 = curr.getY(currIndex1); 394 395 final float pvx = px1 - px0; 396 final float pvy = py1 - py0; 397 final float cvx = cx1 - cx0; 398 final float cvy = cy1 - cy0; 399 mPrevFingerDiffX = pvx; 400 mPrevFingerDiffY = pvy; 401 mCurrFingerDiffX = cvx; 402 mCurrFingerDiffY = cvy; 403 404 mFocusX = cx0 + cvx * 0.5f; 405 mFocusY = cy0 + cvy * 0.5f; 406 mTimeDelta = curr.getEventTime() - prev.getEventTime(); 407 mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1); 408 mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1); 409 } 410 reset()411 private void reset() { 412 if (mPrevEvent != null) { 413 mPrevEvent.recycle(); 414 mPrevEvent = null; 415 } 416 if (mCurrEvent != null) { 417 mCurrEvent.recycle(); 418 mCurrEvent = null; 419 } 420 mGestureInProgress = false; 421 mActiveId0 = -1; 422 mActiveId1 = -1; 423 mInvalidGesture = false; 424 } 425 426 /** 427 * Returns {@code true} if a two-finger scale gesture is in progress. 428 * @return {@code true} if a scale gesture is in progress, {@code false} otherwise. 429 */ isInProgress()430 public boolean isInProgress() { 431 return mGestureInProgress; 432 } 433 434 /** 435 * Get the X coordinate of the current gesture's focal point. 436 * If a gesture is in progress, the focal point is directly between 437 * the two pointers forming the gesture. 438 * If a gesture is ending, the focal point is the location of the 439 * remaining pointer on the screen. 440 * If {@link #isInProgress()} would return false, the result of this 441 * function is undefined. 442 * 443 * @return X coordinate of the focal point in pixels. 444 */ getFocusX()445 public float getFocusX() { 446 return mFocusX; 447 } 448 449 /** 450 * Get the Y coordinate of the current gesture's focal point. 451 * If a gesture is in progress, the focal point is directly between 452 * the two pointers forming the gesture. 453 * If a gesture is ending, the focal point is the location of the 454 * remaining pointer on the screen. 455 * If {@link #isInProgress()} would return false, the result of this 456 * function is undefined. 457 * 458 * @return Y coordinate of the focal point in pixels. 459 */ getFocusY()460 public float getFocusY() { 461 return mFocusY; 462 } 463 464 /** 465 * Return the current distance between the two pointers forming the 466 * gesture in progress. 467 * 468 * @return Distance between pointers in pixels. 469 */ getCurrentSpan()470 public float getCurrentSpan() { 471 if (mCurrLen == -1) { 472 final float cvx = mCurrFingerDiffX; 473 final float cvy = mCurrFingerDiffY; 474 mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy); 475 } 476 return mCurrLen; 477 } 478 479 /** 480 * Return the current x distance between the two pointers forming the 481 * gesture in progress. 482 * 483 * @return Distance between pointers in pixels. 484 */ getCurrentSpanX()485 public float getCurrentSpanX() { 486 return mCurrFingerDiffX; 487 } 488 489 /** 490 * Return the current y distance between the two pointers forming the 491 * gesture in progress. 492 * 493 * @return Distance between pointers in pixels. 494 */ getCurrentSpanY()495 public float getCurrentSpanY() { 496 return mCurrFingerDiffY; 497 } 498 499 /** 500 * Return the previous distance between the two pointers forming the 501 * gesture in progress. 502 * 503 * @return Previous distance between pointers in pixels. 504 */ getPreviousSpan()505 public float getPreviousSpan() { 506 if (mPrevLen == -1) { 507 final float pvx = mPrevFingerDiffX; 508 final float pvy = mPrevFingerDiffY; 509 mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy); 510 } 511 return mPrevLen; 512 } 513 514 /** 515 * Return the previous x distance between the two pointers forming the 516 * gesture in progress. 517 * 518 * @return Previous distance between pointers in pixels. 519 */ getPreviousSpanX()520 public float getPreviousSpanX() { 521 return mPrevFingerDiffX; 522 } 523 524 /** 525 * Return the previous y distance between the two pointers forming the 526 * gesture in progress. 527 * 528 * @return Previous distance between pointers in pixels. 529 */ getPreviousSpanY()530 public float getPreviousSpanY() { 531 return mPrevFingerDiffY; 532 } 533 534 /** 535 * Return the scaling factor from the previous scale event to the current 536 * event. This value is defined as 537 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 538 * 539 * @return The current scaling factor. 540 */ getScaleFactor()541 public float getScaleFactor() { 542 if (mScaleFactor == -1) { 543 mScaleFactor = getCurrentSpan() / getPreviousSpan(); 544 } 545 return mScaleFactor; 546 } 547 548 /** 549 * Return the time difference in milliseconds between the previous 550 * accepted scaling event and the current scaling event. 551 * 552 * @return Time difference since the last scaling event in milliseconds. 553 */ getTimeDelta()554 public long getTimeDelta() { 555 return mTimeDelta; 556 } 557 558 /** 559 * Return the event time of the current event being processed. 560 * 561 * @return Current event time in milliseconds. 562 */ getEventTime()563 public long getEventTime() { 564 return mCurrEvent.getEventTime(); 565 } 566 } 567