FileDocCategorySizeDatePackage
ScaleGestureDetector.javaAPI DocAndroid 5.1 API23241Thu Mar 12 22:22:10 GMT 2015android.view

ScaleGestureDetector

public class ScaleGestureDetector extends Object
Detects scaling transformation gestures using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} callback will notify users when a particular gesture event has occurred. This class should only be used with {@link MotionEvent}s reported via touch. To use this class:
  • Create an instance of the {@code ScaleGestureDetector} for your {@link View}
  • In the {@link View#onTouchEvent(MotionEvent)} method ensure you call {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will be executed when the events occur.

Fields Summary
private static final String
TAG
private final android.content.Context
mContext
private final OnScaleGestureListener
mListener
private float
mFocusX
private float
mFocusY
private boolean
mQuickScaleEnabled
private float
mCurrSpan
private float
mPrevSpan
private float
mInitialSpan
private float
mCurrSpanX
private float
mCurrSpanY
private float
mPrevSpanX
private float
mPrevSpanY
private long
mCurrTime
private long
mPrevTime
private boolean
mInProgress
private int
mSpanSlop
private int
mMinSpan
private float
mTouchUpper
private float
mTouchLower
private float
mTouchHistoryLastAccepted
private int
mTouchHistoryDirection
private long
mTouchHistoryLastAcceptedTime
private int
mTouchMinMajor
private MotionEvent
mDoubleTapEvent
private int
mDoubleTapMode
private final android.os.Handler
mHandler
private static final long
TOUCH_STABILIZE_TIME
private static final int
DOUBLE_TAP_MODE_NONE
private static final int
DOUBLE_TAP_MODE_IN_PROGRESS
private static final float
SCALE_FACTOR
private final InputEventConsistencyVerifier
mInputEventConsistencyVerifier
Consistency verifier for debugging purposes.
private GestureDetector
mGestureDetector
private boolean
mEventBeforeOrAboveStartingGestureEvent
Constructors Summary
public ScaleGestureDetector(android.content.Context context, OnScaleGestureListener listener)
Creates a ScaleGestureDetector with the supplied listener. You may only use this constructor from a {@link android.os.Looper Looper} thread.

param
context the application's context
param
listener the listener invoked for all the callbacks, this must not be null.
throws
NullPointerException if {@code listener} is null.


                                                      
         
        this(context, listener, null);
    
public ScaleGestureDetector(android.content.Context context, OnScaleGestureListener listener, android.os.Handler handler)
Creates a ScaleGestureDetector with the supplied listener.

see
android.os.Handler#Handler()
param
context the application's context
param
listener the listener invoked for all the callbacks, this must not be null.
param
handler the handler to use for running deferred listener events.
throws
NullPointerException if {@code listener} is null.

        mContext = context;
        mListener = listener;
        mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;

        final Resources res = context.getResources();
        mTouchMinMajor = res.getDimensionPixelSize(
                com.android.internal.R.dimen.config_minScalingTouchMajor);
        mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
        mHandler = handler;
        // Quick scale is enabled by default after JB_MR2
        if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) {
            setQuickScaleEnabled(true);
        }
    
Methods Summary
private voidaddTouchHistory(MotionEvent ev)
The touchMajor/touchMinor elements of a MotionEvent can flutter/jitter on some hardware/driver combos. Smooth it out to get kinder, gentler behavior.

param
ev MotionEvent to add to the ongoing history

        final long currentTime = SystemClock.uptimeMillis();
        final int count = ev.getPointerCount();
        boolean accept = currentTime - mTouchHistoryLastAcceptedTime >= TOUCH_STABILIZE_TIME;
        float total = 0;
        int sampleCount = 0;
        for (int i = 0; i < count; i++) {
            final boolean hasLastAccepted = !Float.isNaN(mTouchHistoryLastAccepted);
            final int historySize = ev.getHistorySize();
            final int pointerSampleCount = historySize + 1;
            for (int h = 0; h < pointerSampleCount; h++) {
                float major;
                if (h < historySize) {
                    major = ev.getHistoricalTouchMajor(i, h);
                } else {
                    major = ev.getTouchMajor(i);
                }
                if (major < mTouchMinMajor) major = mTouchMinMajor;
                total += major;

                if (Float.isNaN(mTouchUpper) || major > mTouchUpper) {
                    mTouchUpper = major;
                }
                if (Float.isNaN(mTouchLower) || major < mTouchLower) {
                    mTouchLower = major;
                }

                if (hasLastAccepted) {
                    final int directionSig = (int) Math.signum(major - mTouchHistoryLastAccepted);
                    if (directionSig != mTouchHistoryDirection ||
                            (directionSig == 0 && mTouchHistoryDirection == 0)) {
                        mTouchHistoryDirection = directionSig;
                        final long time = h < historySize ? ev.getHistoricalEventTime(h)
                                : ev.getEventTime();
                        mTouchHistoryLastAcceptedTime = time;
                        accept = false;
                    }
                }
            }
            sampleCount += pointerSampleCount;
        }

        final float avg = total / sampleCount;

        if (accept) {
            float newAccepted = (mTouchUpper + mTouchLower + avg) / 3;
            mTouchUpper = (mTouchUpper + newAccepted) / 2;
            mTouchLower = (mTouchLower + newAccepted) / 2;
            mTouchHistoryLastAccepted = newAccepted;
            mTouchHistoryDirection = 0;
            mTouchHistoryLastAcceptedTime = ev.getEventTime();
        }
    
private voidclearTouchHistory()
Clear all touch history tracking. Useful in ACTION_CANCEL or ACTION_UP.

see
#addTouchHistory(MotionEvent)

        mTouchUpper = Float.NaN;
        mTouchLower = Float.NaN;
        mTouchHistoryLastAccepted = Float.NaN;
        mTouchHistoryDirection = 0;
        mTouchHistoryLastAcceptedTime = 0;
    
public floatgetCurrentSpan()
Return the average distance between each of the pointers forming the gesture in progress through the focal point.

return
Distance between pointers in pixels.

        return mCurrSpan;
    
public floatgetCurrentSpanX()
Return the average X distance between each of the pointers forming the gesture in progress through the focal point.

return
Distance between pointers in pixels.

        return mCurrSpanX;
    
public floatgetCurrentSpanY()
Return the average Y distance between each of the pointers forming the gesture in progress through the focal point.

return
Distance between pointers in pixels.

        return mCurrSpanY;
    
public longgetEventTime()
Return the event time of the current event being processed.

return
Current event time in milliseconds.

        return mCurrTime;
    
public floatgetFocusX()
Get the X coordinate of the current gesture's focal point. If a gesture is in progress, the focal point is between each of the pointers forming the gesture. If {@link #isInProgress()} would return false, the result of this function is undefined.

return
X coordinate of the focal point in pixels.

        return mFocusX;
    
public floatgetFocusY()
Get the Y coordinate of the current gesture's focal point. If a gesture is in progress, the focal point is between each of the pointers forming the gesture. If {@link #isInProgress()} would return false, the result of this function is undefined.

return
Y coordinate of the focal point in pixels.

        return mFocusY;
    
public floatgetPreviousSpan()
Return the previous average distance between each of the pointers forming the gesture in progress through the focal point.

return
Previous distance between pointers in pixels.

        return mPrevSpan;
    
public floatgetPreviousSpanX()
Return the previous average X distance between each of the pointers forming the gesture in progress through the focal point.

return
Previous distance between pointers in pixels.

        return mPrevSpanX;
    
public floatgetPreviousSpanY()
Return the previous average Y distance between each of the pointers forming the gesture in progress through the focal point.

return
Previous distance between pointers in pixels.

        return mPrevSpanY;
    
public floatgetScaleFactor()
Return the scaling factor from the previous scale event to the current event. This value is defined as ({@link #getCurrentSpan()} / {@link #getPreviousSpan()}).

return
The current scaling factor.

        if (inDoubleTapMode()) {
            // Drag is moving up; the further away from the gesture
            // start, the smaller the span should be, the closer,
            // the larger the span, and therefore the larger the scale
            final boolean scaleUp =
                    (mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan < mPrevSpan)) ||
                    (!mEventBeforeOrAboveStartingGestureEvent && (mCurrSpan > mPrevSpan));
            final float spanDiff = (Math.abs(1 - (mCurrSpan / mPrevSpan)) * SCALE_FACTOR);
            return mPrevSpan <= 0 ? 1 : scaleUp ? (1 + spanDiff) : (1 - spanDiff);
        }
        return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
    
public longgetTimeDelta()
Return the time difference in milliseconds between the previous accepted scaling event and the current scaling event.

return
Time difference since the last scaling event in milliseconds.

        return mCurrTime - mPrevTime;
    
private booleaninDoubleTapMode()

        return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS;
    
public booleanisInProgress()
Returns {@code true} if a scale gesture is in progress.

        return mInProgress;
    
public booleanisQuickScaleEnabled()
Return whether the quick scale gesture, in which the user performs a double tap followed by a swipe, should perform scaling. {@see #setQuickScaleEnabled(boolean)}.

        return mQuickScaleEnabled;
    
public booleanonTouchEvent(MotionEvent event)
Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener} when appropriate.

Applications should pass a complete and consistent event stream to this method. A complete and consistent event stream involves all MotionEvents from the initial ACTION_DOWN to the final ACTION_UP or ACTION_CANCEL.

param
event The event to process
return
true if the event was processed and the detector wants to receive the rest of the MotionEvents in this event stream.

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        mCurrTime = event.getEventTime();

        final int action = event.getActionMasked();

        // Forward the event to check for double tap gesture
        if (mQuickScaleEnabled) {
            mGestureDetector.onTouchEvent(event);
        }

        final boolean streamComplete = action == MotionEvent.ACTION_UP ||
                action == MotionEvent.ACTION_CANCEL;

        if (action == MotionEvent.ACTION_DOWN || streamComplete) {
            // Reset any scale in progress with the listener.
            // If it's an ACTION_DOWN we're beginning a new event stream.
            // This means the app probably didn't give us all the events. Shame on it.
            if (mInProgress) {
                mListener.onScaleEnd(this);
                mInProgress = false;
                mInitialSpan = 0;
                mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
            } else if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS && streamComplete) {
                mInProgress = false;
                mInitialSpan = 0;
                mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
            }

            if (streamComplete) {
                clearTouchHistory();
                return true;
            }
        }

        final boolean configChanged = action == MotionEvent.ACTION_DOWN ||
                action == MotionEvent.ACTION_POINTER_UP ||
                action == MotionEvent.ACTION_POINTER_DOWN;


        final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
        final int skipIndex = pointerUp ? event.getActionIndex() : -1;

        // Determine focal point
        float sumX = 0, sumY = 0;
        final int count = event.getPointerCount();
        final int div = pointerUp ? count - 1 : count;
        final float focusX;
        final float focusY;
        if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) {
            // In double tap mode, the focal pt is always where the double tap
            // gesture started
            focusX = mDoubleTapEvent.getX();
            focusY = mDoubleTapEvent.getY();
            if (event.getY() < focusY) {
                mEventBeforeOrAboveStartingGestureEvent = true;
            } else {
                mEventBeforeOrAboveStartingGestureEvent = false;
            }
        } else {
            for (int i = 0; i < count; i++) {
                if (skipIndex == i) continue;
                sumX += event.getX(i);
                sumY += event.getY(i);
            }

            focusX = sumX / div;
            focusY = sumY / div;
        }

        addTouchHistory(event);

        // Determine average deviation from focal point
        float devSumX = 0, devSumY = 0;
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) continue;

            // Convert the resulting diameter into a radius.
            final float touchSize = mTouchHistoryLastAccepted / 2;
            devSumX += Math.abs(event.getX(i) - focusX) + touchSize;
            devSumY += Math.abs(event.getY(i) - focusY) + touchSize;
        }
        final float devX = devSumX / div;
        final float devY = devSumY / div;

        // Span is the average distance between touch points through the focal point;
        // i.e. the diameter of the circle with a radius of the average deviation from
        // the focal point.
        final float spanX = devX * 2;
        final float spanY = devY * 2;
        final float span;
        if (inDoubleTapMode()) {
            span = spanY;
        } else {
            span = FloatMath.sqrt(spanX * spanX + spanY * spanY);
        }

        // Dispatch begin/end events as needed.
        // If the configuration changes, notify the app to reset its current state by beginning
        // a fresh scale event stream.
        final boolean wasInProgress = mInProgress;
        mFocusX = focusX;
        mFocusY = focusY;
        if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) {
            mListener.onScaleEnd(this);
            mInProgress = false;
            mInitialSpan = span;
            mDoubleTapMode = DOUBLE_TAP_MODE_NONE;
        }
        if (configChanged) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mInitialSpan = mPrevSpan = mCurrSpan = span;
        }

        final int minSpan = inDoubleTapMode() ? mSpanSlop : mMinSpan;
        if (!mInProgress && span >=  minSpan &&
                (wasInProgress || Math.abs(span - mInitialSpan) > mSpanSlop)) {
            mPrevSpanX = mCurrSpanX = spanX;
            mPrevSpanY = mCurrSpanY = spanY;
            mPrevSpan = mCurrSpan = span;
            mPrevTime = mCurrTime;
            mInProgress = mListener.onScaleBegin(this);
        }

        // Handle motion; focal point and span/scale factor are changing.
        if (action == MotionEvent.ACTION_MOVE) {
            mCurrSpanX = spanX;
            mCurrSpanY = spanY;
            mCurrSpan = span;

            boolean updatePrev = true;

            if (mInProgress) {
                updatePrev = mListener.onScale(this);
            }

            if (updatePrev) {
                mPrevSpanX = mCurrSpanX;
                mPrevSpanY = mCurrSpanY;
                mPrevSpan = mCurrSpan;
                mPrevTime = mCurrTime;
            }
        }

        return true;
    
public voidsetQuickScaleEnabled(boolean scales)
Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks when the user performs a doubleTap followed by a swipe. Note that this is enabled by default if the app targets API 19 and newer.

param
scales true to enable quick scaling, false to disable

        mQuickScaleEnabled = scales;
        if (mQuickScaleEnabled && mGestureDetector == null) {
            GestureDetector.SimpleOnGestureListener gestureListener =
                    new GestureDetector.SimpleOnGestureListener() {
                        @Override
                        public boolean onDoubleTap(MotionEvent e) {
                            // Double tap: start watching for a swipe
                            mDoubleTapEvent = e;
                            mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS;
                            return true;
                        }
                    };
            mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler);
        }