ScaleGestureDetector.java revision d0197f3669efda060c7ee2069ff41bd970fd6d9c
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 17package android.view; 18 19import android.content.Context; 20import android.util.DisplayMetrics; 21import android.util.FloatMath; 22import 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 */ 39public 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 */ 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 */ 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 */ 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 111 public boolean onScale(ScaleGestureDetector detector) { 112 return false; 113 } 114 115 public boolean onScaleBegin(ScaleGestureDetector detector) { 116 return true; 117 } 118 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 final float mEdgeSlop; 156 private float mRightSlopEdge; 157 private float mBottomSlopEdge; 158 private boolean mSloppyGesture; 159 private boolean mInvalidGesture; 160 161 // Pointer IDs currently responsible for the two fingers controlling the gesture 162 private int mActiveId0; 163 private int mActiveId1; 164 private boolean mActive0MostRecent; 165 166 public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { 167 ViewConfiguration config = ViewConfiguration.get(context); 168 mContext = context; 169 mListener = listener; 170 mEdgeSlop = config.getScaledEdgeSlop(); 171 } 172 173 public boolean onTouchEvent(MotionEvent event) { 174 final int action = event.getActionMasked(); 175 boolean handled = true; 176 177 if (action == MotionEvent.ACTION_DOWN) { 178 reset(); // Start fresh 179 } 180 181 if (mInvalidGesture) return false; 182 183 if (!mGestureInProgress) { 184 switch (action) { 185 case MotionEvent.ACTION_DOWN: { 186 mActiveId0 = event.getPointerId(0); 187 mActive0MostRecent = true; 188 } 189 break; 190 191 case MotionEvent.ACTION_UP: 192 reset(); 193 break; 194 195 case MotionEvent.ACTION_POINTER_DOWN: { 196 // We have a new multi-finger gesture 197 198 // as orientation can change, query the metrics in touch down 199 DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); 200 mRightSlopEdge = metrics.widthPixels - mEdgeSlop; 201 mBottomSlopEdge = metrics.heightPixels - mEdgeSlop; 202 203 if (mPrevEvent != null) mPrevEvent.recycle(); 204 mPrevEvent = MotionEvent.obtain(event); 205 mTimeDelta = 0; 206 207 int index1 = event.getActionIndex(); 208 int index0 = event.findPointerIndex(mActiveId0); 209 mActiveId1 = event.getPointerId(index1); 210 if (index0 < 0 || index0 == index1) { 211 // Probably someone sending us a broken event stream. 212 index0 = findNewActiveIndex(event, index0 == index1 ? -1 : mActiveId1, index0); 213 mActiveId0 = event.getPointerId(index0); 214 } 215 mActive0MostRecent = false; 216 217 setContext(event); 218 219 // Check if we have a sloppy gesture. If so, delay 220 // the beginning of the gesture until we're sure that's 221 // what the user wanted. Sloppy gestures can happen if the 222 // edge of the user's hand is touching the screen, for example. 223 final float edgeSlop = mEdgeSlop; 224 final float rightSlop = mRightSlopEdge; 225 final float bottomSlop = mBottomSlopEdge; 226 float x0 = getRawX(event, index0); 227 float y0 = getRawY(event, index0); 228 float x1 = getRawX(event, index1); 229 float y1 = getRawY(event, index1); 230 231 boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop 232 || x0 > rightSlop || y0 > bottomSlop; 233 boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop 234 || x1 > rightSlop || y1 > bottomSlop; 235 236 if (p0sloppy && p1sloppy) { 237 mFocusX = -1; 238 mFocusY = -1; 239 mSloppyGesture = true; 240 } else if (p0sloppy) { 241 mFocusX = event.getX(index1); 242 mFocusY = event.getY(index1); 243 mSloppyGesture = true; 244 } else if (p1sloppy) { 245 mFocusX = event.getX(index0); 246 mFocusY = event.getY(index0); 247 mSloppyGesture = true; 248 } else { 249 mSloppyGesture = false; 250 mGestureInProgress = mListener.onScaleBegin(this); 251 } 252 } 253 break; 254 255 case MotionEvent.ACTION_MOVE: 256 if (mSloppyGesture) { 257 // Initiate sloppy gestures if we've moved outside of the slop area. 258 final float edgeSlop = mEdgeSlop; 259 final float rightSlop = mRightSlopEdge; 260 final float bottomSlop = mBottomSlopEdge; 261 int index0 = event.findPointerIndex(mActiveId0); 262 int index1 = event.findPointerIndex(mActiveId1); 263 264 float x0 = getRawX(event, index0); 265 float y0 = getRawY(event, index0); 266 float x1 = getRawX(event, index1); 267 float y1 = getRawY(event, index1); 268 269 boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop 270 || x0 > rightSlop || y0 > bottomSlop; 271 boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop 272 || x1 > rightSlop || y1 > bottomSlop; 273 274 if (p0sloppy) { 275 // Do we have a different pointer that isn't sloppy? 276 int index = findNewActiveIndex(event, mActiveId1, index0); 277 if (index >= 0) { 278 index0 = index; 279 mActiveId0 = event.getPointerId(index); 280 x0 = getRawX(event, index); 281 y0 = getRawY(event, index); 282 p0sloppy = false; 283 } 284 } 285 286 if (p1sloppy) { 287 // Do we have a different pointer that isn't sloppy? 288 int index = findNewActiveIndex(event, mActiveId0, index1); 289 if (index >= 0) { 290 index1 = index; 291 mActiveId1 = event.getPointerId(index); 292 x1 = getRawX(event, index); 293 y1 = getRawY(event, index); 294 p1sloppy = false; 295 } 296 } 297 298 if(p0sloppy && p1sloppy) { 299 mFocusX = -1; 300 mFocusY = -1; 301 } else if (p0sloppy) { 302 mFocusX = event.getX(index1); 303 mFocusY = event.getY(index1); 304 } else if (p1sloppy) { 305 mFocusX = event.getX(index0); 306 mFocusY = event.getY(index0); 307 } else { 308 mSloppyGesture = false; 309 mGestureInProgress = mListener.onScaleBegin(this); 310 } 311 } 312 break; 313 314 case MotionEvent.ACTION_POINTER_UP: 315 if (mSloppyGesture) { 316 final int pointerCount = event.getPointerCount(); 317 final int actionIndex = event.getActionIndex(); 318 final int actionId = event.getPointerId(actionIndex); 319 320 if (pointerCount > 2) { 321 if (actionId == mActiveId0) { 322 final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex); 323 if (newIndex >= 0) mActiveId0 = event.getPointerId(newIndex); 324 } else if (actionId == mActiveId1) { 325 final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex); 326 if (newIndex >= 0) mActiveId1 = event.getPointerId(newIndex); 327 } 328 } else { 329 // Set focus point to the remaining finger 330 final int index = event.findPointerIndex(actionId == mActiveId0 ? 331 mActiveId1 : mActiveId0); 332 mActiveId0 = event.getPointerId(index); 333 334 mActive0MostRecent = true; 335 mActiveId1 = -1; 336 mFocusX = event.getX(index); 337 mFocusY = event.getY(index); 338 } 339 } 340 break; 341 } 342 } else { 343 // Transform gesture in progress - attempt to handle it 344 switch (action) { 345 case MotionEvent.ACTION_POINTER_DOWN: { 346 // End the old gesture and begin a new one with the most recent two fingers. 347 mListener.onScaleEnd(this); 348 final int oldActive0 = mActiveId0; 349 final int oldActive1 = mActiveId1; 350 reset(); 351 352 mPrevEvent = MotionEvent.obtain(event); 353 mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1; 354 mActiveId1 = event.getPointerId(event.getActionIndex()); 355 mActive0MostRecent = false; 356 357 int index0 = event.findPointerIndex(mActiveId0); 358 if (index0 < 0 || mActiveId0 == mActiveId1) { 359 // Probably someone sending us a broken event stream. 360 Log.e(TAG, "Got " + MotionEvent.actionToString(action) + 361 " with bad state while a gesture was in progress. " + 362 "Did you forget to pass an event to " + 363 "ScaleGestureDetector#onTouchEvent?"); 364 index0 = findNewActiveIndex(event, 365 mActiveId0 == mActiveId1 ? -1 : mActiveId1, index0); 366 mActiveId0 = event.getPointerId(index0); 367 } 368 369 setContext(event); 370 371 mGestureInProgress = mListener.onScaleBegin(this); 372 } 373 break; 374 375 case MotionEvent.ACTION_POINTER_UP: { 376 final int pointerCount = event.getPointerCount(); 377 final int actionIndex = event.getActionIndex(); 378 final int actionId = event.getPointerId(actionIndex); 379 380 boolean gestureEnded = false; 381 if (pointerCount > 2) { 382 if (actionId == mActiveId0) { 383 final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex); 384 if (newIndex >= 0) { 385 mListener.onScaleEnd(this); 386 mActiveId0 = event.getPointerId(newIndex); 387 mActive0MostRecent = true; 388 mPrevEvent = MotionEvent.obtain(event); 389 setContext(event); 390 mGestureInProgress = mListener.onScaleBegin(this); 391 } else { 392 gestureEnded = true; 393 } 394 } else if (actionId == mActiveId1) { 395 final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex); 396 if (newIndex >= 0) { 397 mListener.onScaleEnd(this); 398 mActiveId1 = event.getPointerId(newIndex); 399 mActive0MostRecent = false; 400 mPrevEvent = MotionEvent.obtain(event); 401 setContext(event); 402 mGestureInProgress = mListener.onScaleBegin(this); 403 } else { 404 gestureEnded = true; 405 } 406 } 407 mPrevEvent.recycle(); 408 mPrevEvent = MotionEvent.obtain(event); 409 setContext(event); 410 } else { 411 gestureEnded = true; 412 } 413 414 if (gestureEnded) { 415 // Gesture ended 416 setContext(event); 417 418 // Set focus point to the remaining finger 419 final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0; 420 final int index = event.findPointerIndex(activeId); 421 mFocusX = event.getX(index); 422 mFocusY = event.getY(index); 423 424 mListener.onScaleEnd(this); 425 reset(); 426 mActiveId0 = activeId; 427 mActive0MostRecent = true; 428 } 429 } 430 break; 431 432 case MotionEvent.ACTION_CANCEL: 433 mListener.onScaleEnd(this); 434 reset(); 435 break; 436 437 case MotionEvent.ACTION_UP: 438 reset(); 439 break; 440 441 case MotionEvent.ACTION_MOVE: { 442 setContext(event); 443 444 // Only accept the event if our relative pressure is within 445 // a certain limit - this can help filter shaky data as a 446 // finger is lifted. 447 if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) { 448 final boolean updatePrevious = mListener.onScale(this); 449 450 if (updatePrevious) { 451 mPrevEvent.recycle(); 452 mPrevEvent = MotionEvent.obtain(event); 453 } 454 } 455 } 456 break; 457 } 458 } 459 return handled; 460 } 461 462 private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int oldIndex) { 463 final int pointerCount = ev.getPointerCount(); 464 465 // It's ok if this isn't found and returns -1, it simply won't match. 466 final int otherActiveIndex = ev.findPointerIndex(otherActiveId); 467 int newActiveIndex = -1; 468 469 // Pick a new id and update tracking state. Only pick pointers not on the slop edges. 470 for (int i = 0; i < pointerCount; i++) { 471 if (i != oldIndex && i != otherActiveIndex) { 472 final float edgeSlop = mEdgeSlop; 473 final float rightSlop = mRightSlopEdge; 474 final float bottomSlop = mBottomSlopEdge; 475 float x = getRawX(ev, i); 476 float y = getRawY(ev, i); 477 if (x >= edgeSlop && y >= edgeSlop && x <= rightSlop && y <= bottomSlop) { 478 newActiveIndex = i; 479 break; 480 } 481 } 482 } 483 484 return newActiveIndex; 485 } 486 487 /** 488 * MotionEvent has no getRawX(int) method; simulate it pending future API approval. 489 */ 490 private static float getRawX(MotionEvent event, int pointerIndex) { 491 if (pointerIndex < 0) return Float.MIN_VALUE; 492 if (pointerIndex == 0) return event.getRawX(); 493 float offset = event.getRawX() - event.getX(); 494 return event.getX(pointerIndex) + offset; 495 } 496 497 /** 498 * MotionEvent has no getRawY(int) method; simulate it pending future API approval. 499 */ 500 private static float getRawY(MotionEvent event, int pointerIndex) { 501 if (pointerIndex < 0) return Float.MIN_VALUE; 502 if (pointerIndex == 0) return event.getRawY(); 503 float offset = event.getRawY() - event.getY(); 504 return event.getY(pointerIndex) + offset; 505 } 506 507 private void setContext(MotionEvent curr) { 508 if (mCurrEvent != null) { 509 mCurrEvent.recycle(); 510 } 511 mCurrEvent = MotionEvent.obtain(curr); 512 513 mCurrLen = -1; 514 mPrevLen = -1; 515 mScaleFactor = -1; 516 517 final MotionEvent prev = mPrevEvent; 518 519 final int prevIndex0 = prev.findPointerIndex(mActiveId0); 520 final int prevIndex1 = prev.findPointerIndex(mActiveId1); 521 final int currIndex0 = curr.findPointerIndex(mActiveId0); 522 final int currIndex1 = curr.findPointerIndex(mActiveId1); 523 524 if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) { 525 mInvalidGesture = true; 526 Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable()); 527 if (mGestureInProgress) { 528 mListener.onScaleEnd(this); 529 } 530 return; 531 } 532 533 final float px0 = prev.getX(prevIndex0); 534 final float py0 = prev.getY(prevIndex0); 535 final float px1 = prev.getX(prevIndex1); 536 final float py1 = prev.getY(prevIndex1); 537 final float cx0 = curr.getX(currIndex0); 538 final float cy0 = curr.getY(currIndex0); 539 final float cx1 = curr.getX(currIndex1); 540 final float cy1 = curr.getY(currIndex1); 541 542 final float pvx = px1 - px0; 543 final float pvy = py1 - py0; 544 final float cvx = cx1 - cx0; 545 final float cvy = cy1 - cy0; 546 mPrevFingerDiffX = pvx; 547 mPrevFingerDiffY = pvy; 548 mCurrFingerDiffX = cvx; 549 mCurrFingerDiffY = cvy; 550 551 mFocusX = cx0 + cvx * 0.5f; 552 mFocusY = cy0 + cvy * 0.5f; 553 mTimeDelta = curr.getEventTime() - prev.getEventTime(); 554 mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1); 555 mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1); 556 } 557 558 private void reset() { 559 if (mPrevEvent != null) { 560 mPrevEvent.recycle(); 561 mPrevEvent = null; 562 } 563 if (mCurrEvent != null) { 564 mCurrEvent.recycle(); 565 mCurrEvent = null; 566 } 567 mSloppyGesture = false; 568 mGestureInProgress = false; 569 mActiveId0 = -1; 570 mActiveId1 = -1; 571 mInvalidGesture = false; 572 } 573 574 /** 575 * Returns {@code true} if a two-finger scale gesture is in progress. 576 * @return {@code true} if a scale gesture is in progress, {@code false} otherwise. 577 */ 578 public boolean isInProgress() { 579 return mGestureInProgress; 580 } 581 582 /** 583 * Get the X coordinate of the current gesture's focal point. 584 * If a gesture is in progress, the focal point is directly between 585 * the two pointers forming the gesture. 586 * If a gesture is ending, the focal point is the location of the 587 * remaining pointer on the screen. 588 * If {@link #isInProgress()} would return false, the result of this 589 * function is undefined. 590 * 591 * @return X coordinate of the focal point in pixels. 592 */ 593 public float getFocusX() { 594 return mFocusX; 595 } 596 597 /** 598 * Get the Y coordinate of the current gesture's focal point. 599 * If a gesture is in progress, the focal point is directly between 600 * the two pointers forming the gesture. 601 * If a gesture is ending, the focal point is the location of the 602 * remaining pointer on the screen. 603 * If {@link #isInProgress()} would return false, the result of this 604 * function is undefined. 605 * 606 * @return Y coordinate of the focal point in pixels. 607 */ 608 public float getFocusY() { 609 return mFocusY; 610 } 611 612 /** 613 * Return the current distance between the two pointers forming the 614 * gesture in progress. 615 * 616 * @return Distance between pointers in pixels. 617 */ 618 public float getCurrentSpan() { 619 if (mCurrLen == -1) { 620 final float cvx = mCurrFingerDiffX; 621 final float cvy = mCurrFingerDiffY; 622 mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy); 623 } 624 return mCurrLen; 625 } 626 627 /** 628 * Return the current x distance between the two pointers forming the 629 * gesture in progress. 630 * 631 * @return Distance between pointers in pixels. 632 */ 633 public float getCurrentSpanX() { 634 return mCurrFingerDiffX; 635 } 636 637 /** 638 * Return the current y distance between the two pointers forming the 639 * gesture in progress. 640 * 641 * @return Distance between pointers in pixels. 642 */ 643 public float getCurrentSpanY() { 644 return mCurrFingerDiffY; 645 } 646 647 /** 648 * Return the previous distance between the two pointers forming the 649 * gesture in progress. 650 * 651 * @return Previous distance between pointers in pixels. 652 */ 653 public float getPreviousSpan() { 654 if (mPrevLen == -1) { 655 final float pvx = mPrevFingerDiffX; 656 final float pvy = mPrevFingerDiffY; 657 mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy); 658 } 659 return mPrevLen; 660 } 661 662 /** 663 * Return the previous x distance between the two pointers forming the 664 * gesture in progress. 665 * 666 * @return Previous distance between pointers in pixels. 667 */ 668 public float getPreviousSpanX() { 669 return mPrevFingerDiffX; 670 } 671 672 /** 673 * Return the previous y distance between the two pointers forming the 674 * gesture in progress. 675 * 676 * @return Previous distance between pointers in pixels. 677 */ 678 public float getPreviousSpanY() { 679 return mPrevFingerDiffY; 680 } 681 682 /** 683 * Return the scaling factor from the previous scale event to the current 684 * event. This value is defined as 685 * ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}). 686 * 687 * @return The current scaling factor. 688 */ 689 public float getScaleFactor() { 690 if (mScaleFactor == -1) { 691 mScaleFactor = getCurrentSpan() / getPreviousSpan(); 692 } 693 return mScaleFactor; 694 } 695 696 /** 697 * Return the time difference in milliseconds between the previous 698 * accepted scaling event and the current scaling event. 699 * 700 * @return Time difference since the last scaling event in milliseconds. 701 */ 702 public long getTimeDelta() { 703 return mTimeDelta; 704 } 705 706 /** 707 * Return the event time of the current event being processed. 708 * 709 * @return Current event time in milliseconds. 710 */ 711 public long getEventTime() { 712 return mCurrEvent.getEventTime(); 713 } 714} 715