Merge "New implementation for ScaleGestureDetector" into jb-mr1-dev

This commit is contained in:
Adam Powell
2012-08-28 18:51:23 -07:00
committed by Android (Google) Code Review

View File

@ -17,14 +17,13 @@
package android.view; package android.view;
import android.content.Context; import android.content.Context;
import android.util.DisplayMetrics;
import android.util.FloatMath; import android.util.FloatMath;
import android.util.Log;
/** /**
* Detects transformation gestures involving more than one pointer ("multitouch") * Detects scaling transformation gestures using the supplied {@link MotionEvent}s.
* using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener} * The {@link OnScaleGestureListener} callback will notify users when a particular
* callback will notify users when a particular gesture event has occurred. * gesture event has occurred.
*
* This class should only be used with {@link MotionEvent}s reported via touch. * This class should only be used with {@link MotionEvent}s reported via touch.
* *
* To use this class: * To use this class:
@ -121,43 +120,21 @@ public class ScaleGestureDetector {
} }
} }
/**
* This value is the threshold ratio between our previous combined pressure
* and the current combined pressure. We will only fire an onScale event if
* the computed ratio between the current and previous event pressures is
* greater than this value. When pressure decreases rapidly between events
* the position values can often be imprecise, as it usually indicates
* that the user is in the process of lifting a pointer off of the device.
* Its value was tuned experimentally.
*/
private static final float PRESSURE_THRESHOLD = 0.67f;
private final Context mContext; private final Context mContext;
private final OnScaleGestureListener mListener; private final OnScaleGestureListener mListener;
private boolean mGestureInProgress;
private MotionEvent mPrevEvent;
private MotionEvent mCurrEvent;
private float mFocusX; private float mFocusX;
private float mFocusY; private float mFocusY;
private float mPrevFingerDiffX;
private float mPrevFingerDiffY;
private float mCurrFingerDiffX;
private float mCurrFingerDiffY;
private float mCurrLen;
private float mPrevLen;
private float mScaleFactor;
private float mCurrPressure;
private float mPrevPressure;
private long mTimeDelta;
private boolean mInvalidGesture; private float mCurrSpan;
private float mPrevSpan;
// Pointer IDs currently responsible for the two fingers controlling the gesture private float mCurrSpanX;
private int mActiveId0; private float mCurrSpanY;
private int mActiveId1; private float mPrevSpanX;
private boolean mActive0MostRecent; private float mPrevSpanY;
private long mCurrTime;
private long mPrevTime;
private boolean mInProgress;
/** /**
* Consistency verifier for debugging purposes. * Consistency verifier for debugging purposes.
@ -171,6 +148,18 @@ public class ScaleGestureDetector {
mListener = listener; mListener = listener;
} }
/**
* Accepts MotionEvents and dispatches events to a {@link OnScaleGestureListener}
* when appropriate.
*
* <p>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.</p>
*
* @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.
*/
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) { if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0); mInputEventConsistencyVerifier.onTouchEvent(event, 0);
@ -178,265 +167,110 @@ public class ScaleGestureDetector {
final int action = event.getActionMasked(); final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) { final boolean streamComplete = action == MotionEvent.ACTION_UP ||
reset(); // Start fresh action == MotionEvent.ACTION_CANCEL;
} if (action == MotionEvent.ACTION_DOWN || streamComplete) {
// Reset any scale in progress with the listener.
boolean handled = true; // If it's an ACTION_DOWN we're beginning a new event stream.
if (mInvalidGesture) { // This means the app probably didn't give us all the events. Shame on it.
handled = false; if (mInProgress) {
} else if (!mGestureInProgress) {
switch (action) {
case MotionEvent.ACTION_DOWN: {
mActiveId0 = event.getPointerId(0);
mActive0MostRecent = true;
}
break;
case MotionEvent.ACTION_UP:
reset();
break;
case MotionEvent.ACTION_POINTER_DOWN: {
// We have a new multi-finger gesture
if (mPrevEvent != null) mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;
int index1 = event.getActionIndex();
int index0 = event.findPointerIndex(mActiveId0);
mActiveId1 = event.getPointerId(index1);
if (index0 < 0 || index0 == index1) {
// Probably someone sending us a broken event stream.
index0 = findNewActiveIndex(event, mActiveId1, -1);
mActiveId0 = event.getPointerId(index0);
}
mActive0MostRecent = false;
setContext(event);
mGestureInProgress = mListener.onScaleBegin(this);
break;
}
}
} else {
// Transform gesture in progress - attempt to handle it
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN: {
// End the old gesture and begin a new one with the most recent two fingers.
mListener.onScaleEnd(this);
final int oldActive0 = mActiveId0;
final int oldActive1 = mActiveId1;
reset();
mPrevEvent = MotionEvent.obtain(event);
mActiveId0 = mActive0MostRecent ? oldActive0 : oldActive1;
mActiveId1 = event.getPointerId(event.getActionIndex());
mActive0MostRecent = false;
int index0 = event.findPointerIndex(mActiveId0);
if (index0 < 0 || mActiveId0 == mActiveId1) {
// Probably someone sending us a broken event stream.
Log.e(TAG, "Got " + MotionEvent.actionToString(action) +
" with bad state while a gesture was in progress. " +
"Did you forget to pass an event to " +
"ScaleGestureDetector#onTouchEvent?");
index0 = findNewActiveIndex(event, mActiveId1, -1);
mActiveId0 = event.getPointerId(index0);
}
setContext(event);
mGestureInProgress = mListener.onScaleBegin(this);
}
break;
case MotionEvent.ACTION_POINTER_UP: {
final int pointerCount = event.getPointerCount();
final int actionIndex = event.getActionIndex();
final int actionId = event.getPointerId(actionIndex);
boolean gestureEnded = false;
if (pointerCount > 2) {
if (actionId == mActiveId0) {
final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex);
if (newIndex >= 0) {
mListener.onScaleEnd(this);
mActiveId0 = event.getPointerId(newIndex);
mActive0MostRecent = true;
mPrevEvent = MotionEvent.obtain(event);
setContext(event);
mGestureInProgress = mListener.onScaleBegin(this);
} else {
gestureEnded = true;
}
} else if (actionId == mActiveId1) {
final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex);
if (newIndex >= 0) {
mListener.onScaleEnd(this);
mActiveId1 = event.getPointerId(newIndex);
mActive0MostRecent = false;
mPrevEvent = MotionEvent.obtain(event);
setContext(event);
mGestureInProgress = mListener.onScaleBegin(this);
} else {
gestureEnded = true;
}
}
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
setContext(event);
} else {
gestureEnded = true;
}
if (gestureEnded) {
// Gesture ended
setContext(event);
// Set focus point to the remaining finger
final int activeId = actionId == mActiveId0 ? mActiveId1 : mActiveId0;
final int index = event.findPointerIndex(activeId);
mFocusX = event.getX(index);
mFocusY = event.getY(index);
mListener.onScaleEnd(this);
reset();
mActiveId0 = activeId;
mActive0MostRecent = true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
mListener.onScaleEnd(this);
reset();
break;
case MotionEvent.ACTION_UP:
reset();
break;
case MotionEvent.ACTION_MOVE: {
setContext(event);
// Only accept the event if our relative pressure is within
// a certain limit - this can help filter shaky data as a
// finger is lifted.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
final boolean updatePrevious = mListener.onScale(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
}
break;
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return handled;
}
private int findNewActiveIndex(MotionEvent ev, int otherActiveId, int removedPointerIndex) {
final int pointerCount = ev.getPointerCount();
// It's ok if this isn't found and returns -1, it simply won't match.
final int otherActiveIndex = ev.findPointerIndex(otherActiveId);
// Pick a new id and update tracking state.
for (int i = 0; i < pointerCount; i++) {
if (i != removedPointerIndex && i != otherActiveIndex) {
return i;
}
}
return -1;
}
private void setContext(MotionEvent curr) {
if (mCurrEvent != null) {
mCurrEvent.recycle();
}
mCurrEvent = MotionEvent.obtain(curr);
mCurrLen = -1;
mPrevLen = -1;
mScaleFactor = -1;
final MotionEvent prev = mPrevEvent;
final int prevIndex0 = prev.findPointerIndex(mActiveId0);
final int prevIndex1 = prev.findPointerIndex(mActiveId1);
final int currIndex0 = curr.findPointerIndex(mActiveId0);
final int currIndex1 = curr.findPointerIndex(mActiveId1);
if (prevIndex0 < 0 || prevIndex1 < 0 || currIndex0 < 0 || currIndex1 < 0) {
mInvalidGesture = true;
Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable());
if (mGestureInProgress) {
mListener.onScaleEnd(this); mListener.onScaleEnd(this);
mInProgress = false;
}
if (streamComplete) {
return true;
} }
return;
} }
final float px0 = prev.getX(prevIndex0); final boolean configChanged =
final float py0 = prev.getY(prevIndex0); action == MotionEvent.ACTION_POINTER_UP ||
final float px1 = prev.getX(prevIndex1); action == MotionEvent.ACTION_POINTER_DOWN;
final float py1 = prev.getY(prevIndex1); final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP;
final float cx0 = curr.getX(currIndex0); final int skipIndex = pointerUp ? event.getActionIndex() : -1;
final float cy0 = curr.getY(currIndex0);
final float cx1 = curr.getX(currIndex1);
final float cy1 = curr.getY(currIndex1);
final float pvx = px1 - px0; // Determine focal point
final float pvy = py1 - py0; float sumX = 0, sumY = 0;
final float cvx = cx1 - cx0; final int count = event.getPointerCount();
final float cvy = cy1 - cy0; for (int i = 0; i < count; i++) {
mPrevFingerDiffX = pvx; if (skipIndex == i) continue;
mPrevFingerDiffY = pvy; sumX += event.getX(i);
mCurrFingerDiffX = cvx; sumY += event.getY(i);
mCurrFingerDiffY = cvy;
mFocusX = cx0 + cvx * 0.5f;
mFocusY = cy0 + cvy * 0.5f;
mTimeDelta = curr.getEventTime() - prev.getEventTime();
mCurrPressure = curr.getPressure(currIndex0) + curr.getPressure(currIndex1);
mPrevPressure = prev.getPressure(prevIndex0) + prev.getPressure(prevIndex1);
}
private void reset() {
if (mPrevEvent != null) {
mPrevEvent.recycle();
mPrevEvent = null;
} }
if (mCurrEvent != null) { final int div = pointerUp ? count - 1 : count;
mCurrEvent.recycle(); final float focusX = sumX / div;
mCurrEvent = null; final float focusY = sumY / div;
// Determine average deviation from focal point
float devSumX = 0, devSumY = 0;
for (int i = 0; i < count; i++) {
if (skipIndex == i) continue;
devSumX += Math.abs(event.getX(i) - focusX);
devSumY += Math.abs(event.getY(i) - focusY);
} }
mGestureInProgress = false; final float devX = devSumX / div;
mActiveId0 = -1; final float devY = devSumY / div;
mActiveId1 = -1;
mInvalidGesture = false; // 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 = 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.
if (mInProgress && (span == 0 || configChanged)) {
mListener.onScaleEnd(this);
mInProgress = false;
}
if (configChanged) {
mPrevSpanX = mCurrSpanX = spanX;
mPrevSpanY = mCurrSpanY = spanY;
mPrevSpan = mCurrSpan = span;
}
if (!mInProgress && span != 0) {
mFocusX = focusX;
mFocusY = focusY;
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;
mFocusX = focusX;
mFocusY = focusY;
boolean updatePrev = true;
if (mInProgress) {
updatePrev = mListener.onScale(this);
}
if (updatePrev) {
mPrevSpanX = mCurrSpanX;
mPrevSpanY = mCurrSpanY;
mPrevSpan = mCurrSpan;
}
}
return true;
} }
/** /**
* Returns {@code true} if a two-finger scale gesture is in progress. * Returns {@code true} if a scale gesture is in progress.
* @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
*/ */
public boolean isInProgress() { public boolean isInProgress() {
return mGestureInProgress; return mInProgress;
} }
/** /**
* Get the X coordinate of the current gesture's focal point. * Get the X coordinate of the current gesture's focal point.
* If a gesture is in progress, the focal point is directly between * If a gesture is in progress, the focal point is between
* the two pointers forming the gesture. * each of the pointers forming the gesture.
* If a gesture is ending, the focal point is the location of the *
* remaining pointer on the screen.
* If {@link #isInProgress()} would return false, the result of this * If {@link #isInProgress()} would return false, the result of this
* function is undefined. * function is undefined.
* *
@ -448,10 +282,9 @@ public class ScaleGestureDetector {
/** /**
* Get the Y coordinate of the current gesture's focal point. * Get the Y coordinate of the current gesture's focal point.
* If a gesture is in progress, the focal point is directly between * If a gesture is in progress, the focal point is between
* the two pointers forming the gesture. * each of the pointers forming the gesture.
* If a gesture is ending, the focal point is the location of the *
* remaining pointer on the screen.
* If {@link #isInProgress()} would return false, the result of this * If {@link #isInProgress()} would return false, the result of this
* function is undefined. * function is undefined.
* *
@ -462,73 +295,63 @@ public class ScaleGestureDetector {
} }
/** /**
* Return the current distance between the two pointers forming the * Return the average distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Distance between pointers in pixels. * @return Distance between pointers in pixels.
*/ */
public float getCurrentSpan() { public float getCurrentSpan() {
if (mCurrLen == -1) { return mCurrSpan;
final float cvx = mCurrFingerDiffX;
final float cvy = mCurrFingerDiffY;
mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
}
return mCurrLen;
} }
/** /**
* Return the current x distance between the two pointers forming the * Return the average X distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Distance between pointers in pixels. * @return Distance between pointers in pixels.
*/ */
public float getCurrentSpanX() { public float getCurrentSpanX() {
return mCurrFingerDiffX; return mCurrSpanX;
} }
/** /**
* Return the current y distance between the two pointers forming the * Return the average Y distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Distance between pointers in pixels. * @return Distance between pointers in pixels.
*/ */
public float getCurrentSpanY() { public float getCurrentSpanY() {
return mCurrFingerDiffY; return mCurrSpanY;
} }
/** /**
* Return the previous distance between the two pointers forming the * Return the previous average distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Previous distance between pointers in pixels. * @return Previous distance between pointers in pixels.
*/ */
public float getPreviousSpan() { public float getPreviousSpan() {
if (mPrevLen == -1) { return mPrevSpan;
final float pvx = mPrevFingerDiffX;
final float pvy = mPrevFingerDiffY;
mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
}
return mPrevLen;
} }
/** /**
* Return the previous x distance between the two pointers forming the * Return the previous average X distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Previous distance between pointers in pixels. * @return Previous distance between pointers in pixels.
*/ */
public float getPreviousSpanX() { public float getPreviousSpanX() {
return mPrevFingerDiffX; return mPrevSpanX;
} }
/** /**
* Return the previous y distance between the two pointers forming the * Return the previous average Y distance between each of the pointers forming the
* gesture in progress. * gesture in progress through the focal point.
* *
* @return Previous distance between pointers in pixels. * @return Previous distance between pointers in pixels.
*/ */
public float getPreviousSpanY() { public float getPreviousSpanY() {
return mPrevFingerDiffY; return mPrevSpanY;
} }
/** /**
@ -539,10 +362,7 @@ public class ScaleGestureDetector {
* @return The current scaling factor. * @return The current scaling factor.
*/ */
public float getScaleFactor() { public float getScaleFactor() {
if (mScaleFactor == -1) { return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
mScaleFactor = getCurrentSpan() / getPreviousSpan();
}
return mScaleFactor;
} }
/** /**
@ -552,7 +372,7 @@ public class ScaleGestureDetector {
* @return Time difference since the last scaling event in milliseconds. * @return Time difference since the last scaling event in milliseconds.
*/ */
public long getTimeDelta() { public long getTimeDelta() {
return mTimeDelta; return mCurrTime - mPrevTime;
} }
/** /**
@ -561,6 +381,6 @@ public class ScaleGestureDetector {
* @return Current event time in milliseconds. * @return Current event time in milliseconds.
*/ */
public long getEventTime() { public long getEventTime() {
return mCurrEvent.getEventTime(); return mCurrTime;
} }
} }