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;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.FloatMath;
import android.util.Log;
/**
* Detects transformation gestures involving more than one pointer ("multitouch")
* using the supplied {@link MotionEvent}s. The {@link OnScaleGestureListener}
* callback will notify users when a particular gesture event has occurred.
* 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:
@ -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 OnScaleGestureListener mListener;
private boolean mGestureInProgress;
private MotionEvent mPrevEvent;
private MotionEvent mCurrEvent;
private float mFocusX;
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;
// Pointer IDs currently responsible for the two fingers controlling the gesture
private int mActiveId0;
private int mActiveId1;
private boolean mActive0MostRecent;
private float mCurrSpan;
private float mPrevSpan;
private float mCurrSpanX;
private float mCurrSpanY;
private float mPrevSpanX;
private float mPrevSpanY;
private long mCurrTime;
private long mPrevTime;
private boolean mInProgress;
/**
* Consistency verifier for debugging purposes.
@ -171,6 +148,18 @@ public class ScaleGestureDetector {
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) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
@ -178,265 +167,110 @@ public class ScaleGestureDetector {
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
reset(); // Start fresh
}
boolean handled = true;
if (mInvalidGesture) {
handled = false;
} 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) {
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;
}
if (streamComplete) {
return true;
}
return;
}
final float px0 = prev.getX(prevIndex0);
final float py0 = prev.getY(prevIndex0);
final float px1 = prev.getX(prevIndex1);
final float py1 = prev.getY(prevIndex1);
final float cx0 = curr.getX(currIndex0);
final float cy0 = curr.getY(currIndex0);
final float cx1 = curr.getX(currIndex1);
final float cy1 = curr.getY(currIndex1);
final boolean configChanged =
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;
final float pvx = px1 - px0;
final float pvy = py1 - py0;
final float cvx = cx1 - cx0;
final float cvy = cy1 - cy0;
mPrevFingerDiffX = pvx;
mPrevFingerDiffY = pvy;
mCurrFingerDiffX = cvx;
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;
// Determine focal point
float sumX = 0, sumY = 0;
final int count = event.getPointerCount();
for (int i = 0; i < count; i++) {
if (skipIndex == i) continue;
sumX += event.getX(i);
sumY += event.getY(i);
}
if (mCurrEvent != null) {
mCurrEvent.recycle();
mCurrEvent = null;
final int div = pointerUp ? count - 1 : count;
final float focusX = sumX / div;
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;
mActiveId0 = -1;
mActiveId1 = -1;
mInvalidGesture = false;
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 = 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.
* @return {@code true} if a scale gesture is in progress, {@code false} otherwise.
* Returns {@code true} if a scale gesture is in progress.
*/
public boolean isInProgress() {
return mGestureInProgress;
return mInProgress;
}
/**
* Get the X coordinate of the current gesture's focal point.
* If a gesture is in progress, the focal point is directly between
* the two pointers forming the gesture.
* If a gesture is ending, the focal point is the location of the
* remaining pointer on the screen.
* 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.
*
@ -448,10 +282,9 @@ public class ScaleGestureDetector {
/**
* Get the Y coordinate of the current gesture's focal point.
* If a gesture is in progress, the focal point is directly between
* the two pointers forming the gesture.
* If a gesture is ending, the focal point is the location of the
* remaining pointer on the screen.
* 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.
*
@ -462,73 +295,63 @@ public class ScaleGestureDetector {
}
/**
* Return the current distance between the two pointers forming the
* gesture in progress.
* Return the average distance between each of the pointers forming the
* gesture in progress through the focal point.
*
* @return Distance between pointers in pixels.
*/
public float getCurrentSpan() {
if (mCurrLen == -1) {
final float cvx = mCurrFingerDiffX;
final float cvy = mCurrFingerDiffY;
mCurrLen = FloatMath.sqrt(cvx*cvx + cvy*cvy);
}
return mCurrLen;
return mCurrSpan;
}
/**
* Return the current x distance between the two pointers forming the
* gesture in progress.
* 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.
*/
public float getCurrentSpanX() {
return mCurrFingerDiffX;
return mCurrSpanX;
}
/**
* Return the current y distance between the two pointers forming the
* gesture in progress.
* 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.
*/
public float getCurrentSpanY() {
return mCurrFingerDiffY;
return mCurrSpanY;
}
/**
* Return the previous distance between the two pointers forming the
* gesture in progress.
* 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.
*/
public float getPreviousSpan() {
if (mPrevLen == -1) {
final float pvx = mPrevFingerDiffX;
final float pvy = mPrevFingerDiffY;
mPrevLen = FloatMath.sqrt(pvx*pvx + pvy*pvy);
}
return mPrevLen;
return mPrevSpan;
}
/**
* Return the previous x distance between the two pointers forming the
* gesture in progress.
* 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.
*/
public float getPreviousSpanX() {
return mPrevFingerDiffX;
return mPrevSpanX;
}
/**
* Return the previous y distance between the two pointers forming the
* gesture in progress.
* 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.
*/
public float getPreviousSpanY() {
return mPrevFingerDiffY;
return mPrevSpanY;
}
/**
@ -539,10 +362,7 @@ public class ScaleGestureDetector {
* @return The current scaling factor.
*/
public float getScaleFactor() {
if (mScaleFactor == -1) {
mScaleFactor = getCurrentSpan() / getPreviousSpan();
}
return mScaleFactor;
return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1;
}
/**
@ -552,7 +372,7 @@ public class ScaleGestureDetector {
* @return Time difference since the last scaling event in milliseconds.
*/
public long getTimeDelta() {
return mTimeDelta;
return mCurrTime - mPrevTime;
}
/**
@ -561,6 +381,6 @@ public class ScaleGestureDetector {
* @return Current event time in milliseconds.
*/
public long getEventTime() {
return mCurrEvent.getEventTime();
return mCurrTime;
}
}