Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- 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.
- * This class should only be used with {@link MotionEvent}s reported via touch.
- *
- * To use this class:
- * <ul>
- * <li>Create an instance of the {@code ScaleGestureDetector} for your
- * {@link View}
- * <li>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.
- * </ul>
- */
- public class ScaleGestureDetector {
- private static final String TAG = "ScaleGestureDetector";
- /**
- * The listener for receiving notifications when gestures occur.
- * If you want to listen for all the different gestures then implement
- * this interface. If you only want to listen for a subset it might
- * be easier to extend {@link SimpleOnScaleGestureListener}.
- *
- * An application will receive events in the following order:
- * <ul>
- * <li>One {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)}
- * <li>Zero or more {@link OnScaleGestureListener#onScale(ScaleGestureDetector)}
- * <li>One {@link OnScaleGestureListener#onScaleEnd(ScaleGestureDetector)}
- * </ul>
- */
- public interface OnScaleGestureListener {
- /**
- * Responds to scaling events for a gesture in progress.
- * Reported by pointer motion.
- *
- * @param detector The detector reporting the event - use this to
- * retrieve extended info about event state.
- * @return Whether or not the detector should consider this event
- * as handled. If an event was not handled, the detector
- * will continue to accumulate movement until an event is
- * handled. This can be useful if an application, for example,
- * only wants to update scaling factors if the change is
- * greater than 0.01.
- */
- public boolean onScale(ScaleGestureDetector detector);
- /**
- * Responds to the beginning of a scaling gesture. Reported by
- * new pointers going down.
- *
- * @param detector The detector reporting the event - use this to
- * retrieve extended info about event state.
- * @return Whether or not the detector should continue recognizing
- * this gesture. For example, if a gesture is beginning
- * with a focal point outside of a region where it makes
- * sense, onScaleBegin() may return false to ignore the
- * rest of the gesture.
- */
- public boolean onScaleBegin(ScaleGestureDetector detector);
- /**
- * Responds to the end of a scale gesture. Reported by existing
- * pointers going up.
- *
- * Once a scale has ended, {@link ScaleGestureDetector#getFocusX()}
- * and {@link ScaleGestureDetector#getFocusY()} will return the location
- * of the pointer remaining on the screen.
- *
- * @param detector The detector reporting the event - use this to
- * retrieve extended info about event state.
- */
- public void onScaleEnd(ScaleGestureDetector detector);
- }
- /**
- * A convenience class to extend when you only want to listen for a subset
- * of scaling-related events. This implements all methods in
- * {@link OnScaleGestureListener} but does nothing.
- * {@link OnScaleGestureListener#onScale(ScaleGestureDetector)} returns
- * {@code false} so that a subclass can retrieve the accumulated scale
- * factor in an overridden onScaleEnd.
- * {@link OnScaleGestureListener#onScaleBegin(ScaleGestureDetector)} returns
- * {@code true}.
- */
- public static class SimpleOnScaleGestureListener implements OnScaleGestureListener {
- public boolean onScale(ScaleGestureDetector detector) {
- return false;
- }
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- return true;
- }
- public void onScaleEnd(ScaleGestureDetector detector) {
- // Intentionally empty
- }
- }
- /**
- * 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 final float mEdgeSlop;
- private float mRightSlopEdge;
- private float mBottomSlopEdge;
- private boolean mSloppyGesture;
- private boolean mInvalidGesture;
- // Pointer IDs currently responsible for the two fingers controlling the gesture
- private int mActiveId0;
- private int mActiveId1;
- private boolean mActive0MostRecent;
- /**
- * Consistency verifier for debugging purposes.
- */
- private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
- InputEventConsistencyVerifier.isInstrumentationEnabled() ?
- new InputEventConsistencyVerifier(this, 0) : null;
- public ScaleGestureDetector(Context context, OnScaleGestureListener listener) {
- ViewConfiguration config = ViewConfiguration.get(context);
- mContext = context;
- mListener = listener;
- mEdgeSlop = config.getScaledEdgeSlop();
- }
- public boolean onTouchEvent(MotionEvent event) {
- if (mInputEventConsistencyVerifier != null) {
- mInputEventConsistencyVerifier.onTouchEvent(event, 0);
- }
- 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
- // as orientation can change, query the metrics in touch down
- DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
- mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
- mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
- 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, index0 == index1 ? -1 : mActiveId1, index0);
- mActiveId0 = event.getPointerId(index0);
- }
- mActive0MostRecent = false;
- setContext(event);
- // Check if we have a sloppy gesture. If so, delay
- // the beginning of the gesture until we're sure that's
- // what the user wanted. Sloppy gestures can happen if the
- // edge of the user's hand is touching the screen, for example.
- final float edgeSlop = mEdgeSlop;
- final float rightSlop = mRightSlopEdge;
- final float bottomSlop = mBottomSlopEdge;
- float x0 = getRawX(event, index0);
- float y0 = getRawY(event, index0);
- float x1 = getRawX(event, index1);
- float y1 = getRawY(event, index1);
- boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
- || x0 > rightSlop || y0 > bottomSlop;
- boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
- || x1 > rightSlop || y1 > bottomSlop;
- if (p0sloppy && p1sloppy) {
- mFocusX = -1;
- mFocusY = -1;
- mSloppyGesture = true;
- } else if (p0sloppy) {
- mFocusX = event.getX(index1);
- mFocusY = event.getY(index1);
- mSloppyGesture = true;
- } else if (p1sloppy) {
- mFocusX = event.getX(index0);
- mFocusY = event.getY(index0);
- mSloppyGesture = true;
- } else {
- mSloppyGesture = false;
- mGestureInProgress = mListener.onScaleBegin(this);
- }
- }
- break;
- case MotionEvent.ACTION_MOVE:
- if (mSloppyGesture) {
- // Initiate sloppy gestures if we've moved outside of the slop area.
- final float edgeSlop = mEdgeSlop;
- final float rightSlop = mRightSlopEdge;
- final float bottomSlop = mBottomSlopEdge;
- int index0 = event.findPointerIndex(mActiveId0);
- int index1 = event.findPointerIndex(mActiveId1);
- float x0 = getRawX(event, index0);
- float y0 = getRawY(event, index0);
- float x1 = getRawX(event, index1);
- float y1 = getRawY(event, index1);
- boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
- || x0 > rightSlop || y0 > bottomSlop;
- boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
- || x1 > rightSlop || y1 > bottomSlop;
- if (p0sloppy) {
- // Do we have a different pointer that isn't sloppy?
- int index = findNewActiveIndex(event, mActiveId1, index0);
- if (index >= 0) {
- index0 = index;
- mActiveId0 = event.getPointerId(index);
- x0 = getRawX(event, index);
- y0 = getRawY(event, index);
- p0sloppy = false;
- }
- }
- if (p1sloppy) {
- // Do we have a different pointer that isn't sloppy?
- int index = findNewActiveIndex(event, mActiveId0, index1);
- if (index >= 0) {
- index1 = index;
- mActiveId1 = event.getPointerId(index);
- x1 = getRawX(event, index);
- y1 = getRawY(event, index);
- p1sloppy = false;
- }
- }
- if(p0sloppy && p1sloppy) {
- mFocusX = -1;
- mFocusY = -1;
- } else if (p0sloppy) {
- mFocusX = event.getX(index1);
- mFocusY = event.getY(index1);
- } else if (p1sloppy) {
- mFocusX = event.getX(index0);
- mFocusY = event.getY(index0);
- } else {
- mSloppyGesture = false;
- mGestureInProgress = mListener.onScaleBegin(this);
- }
- }
- break;
- case MotionEvent.ACTION_POINTER_UP:
- if (mSloppyGesture) {
- final int pointerCount = event.getPointerCount();
- final int actionIndex = event.getActionIndex();
- final int actionId = event.getPointerId(actionIndex);
- if (pointerCount > 2) {
- if (actionId == mActiveId0) {
- final int newIndex = findNewActiveIndex(event, mActiveId1, actionIndex);
- if (newIndex >= 0) mActiveId0 = event.getPointerId(newIndex);
- } else if (actionId == mActiveId1) {
- final int newIndex = findNewActiveIndex(event, mActiveId0, actionIndex);
- if (newIndex >= 0) mActiveId1 = event.getPointerId(newIndex);
- }
- } else {
- // Set focus point to the remaining finger
- final int index = event.findPointerIndex(actionId == mActiveId0 ?
- mActiveId1 : mActiveId0);
- if (index < 0) {
- mInvalidGesture = true;
- Log.e(TAG, "Invalid MotionEvent stream detected.", new Throwable());
- if (mGestureInProgress) {
- mListener.onScaleEnd(this);
- }
- return false;
- }
- mActiveId0 = event.getPointerId(index);
- mActive0MostRecent = true;
- mActiveId1 = -1;
- mFocusX = event.getX(index);
- mFocusY = event.getY(index);
- }
- }
- 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,
- mActiveId0 == mActiveId1 ? -1 : mActiveId1, index0);
- 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 oldIndex) {
- 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);
- int newActiveIndex = -1;
- // Pick a new id and update tracking state. Only pick pointers not on the slop edges.
- for (int i = 0; i < pointerCount; i++) {
- if (i != oldIndex && i != otherActiveIndex) {
- final float edgeSlop = mEdgeSlop;
- final float rightSlop = mRightSlopEdge;
- final float bottomSlop = mBottomSlopEdge;
- float x = getRawX(ev, i);
- float y = getRawY(ev, i);
- if (x >= edgeSlop && y >= edgeSlop && x <= rightSlop && y <= bottomSlop) {
- newActiveIndex = i;
- break;
- }
- }
- }
- return newActiveIndex;
- }
- /**
- * MotionEvent has no getRawX(int) method; simulate it pending future API approval.
- */
- private static float getRawX(MotionEvent event, int pointerIndex) {
- if (pointerIndex < 0) return Float.MIN_VALUE;
- if (pointerIndex == 0) return event.getRawX();
- float offset = event.getRawX() - event.getX();
- return event.getX(pointerIndex) + offset;
- }
- /**
- * MotionEvent has no getRawY(int) method; simulate it pending future API approval.
- */
- private static float getRawY(MotionEvent event, int pointerIndex) {
- if (pointerIndex < 0) return Float.MIN_VALUE;
- if (pointerIndex == 0) return event.getRawY();
- float offset = event.getRawY() - event.getY();
- return event.getY(pointerIndex) + offset;
- }
- 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);
- }
- 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 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;
- }
- if (mCurrEvent != null) {
- mCurrEvent.recycle();
- mCurrEvent = null;
- }
- mSloppyGesture = false;
- mGestureInProgress = false;
- mActiveId0 = -1;
- mActiveId1 = -1;
- mInvalidGesture = false;
- }
- /**
- * 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.
- */
- public boolean isInProgress() {
- return mGestureInProgress;
- }
- /**
- * 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 {@link #isInProgress()} would return false, the result of this
- * function is undefined.
- *
- * @return X coordinate of the focal point in pixels.
- */
- public float getFocusX() {
- return mFocusX;
- }
- /**
- * 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 {@link #isInProgress()} would return false, the result of this
- * function is undefined.
- *
- * @return Y coordinate of the focal point in pixels.
- */
- public float getFocusY() {
- return mFocusY;
- }
- /**
- * Return the current distance between the two pointers forming the
- * gesture in progress.
- *
- * @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 the current x distance between the two pointers forming the
- * gesture in progress.
- *
- * @return Distance between pointers in pixels.
- */
- public float getCurrentSpanX() {
- return mCurrFingerDiffX;
- }
- /**
- * Return the current y distance between the two pointers forming the
- * gesture in progress.
- *
- * @return Distance between pointers in pixels.
- */
- public float getCurrentSpanY() {
- return mCurrFingerDiffY;
- }
- /**
- * Return the previous distance between the two pointers forming the
- * gesture in progress.
- *
- * @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 the previous x distance between the two pointers forming the
- * gesture in progress.
- *
- * @return Previous distance between pointers in pixels.
- */
- public float getPreviousSpanX() {
- return mPrevFingerDiffX;
- }
- /**
- * Return the previous y distance between the two pointers forming the
- * gesture in progress.
- *
- * @return Previous distance between pointers in pixels.
- */
- public float getPreviousSpanY() {
- return mPrevFingerDiffY;
- }
- /**
- * 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.
- */
- public float getScaleFactor() {
- if (mScaleFactor == -1) {
- mScaleFactor = getCurrentSpan() / getPreviousSpan();
- }
- return mScaleFactor;
- }
- /**
- * 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.
- */
- public long getTimeDelta() {
- return mTimeDelta;
- }
- /**
- * Return the event time of the current event being processed.
- *
- * @return Current event time in milliseconds.
- */
- public long getEventTime() {
- return mCurrEvent.getEventTime();
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement