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