/* * * 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 com.fanfou.app.opensource.ui.widget; import android.content.Context; import android.util.DisplayMetrics; import android.util.FloatMath; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; /** * 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 { /** * 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 { @Override public boolean onScale(final ScaleGestureDetector detector) { return false; } @Override public boolean onScaleBegin(final ScaleGestureDetector detector) { return true; } @Override public void onScaleEnd(final 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; /** * MotionEvent has no getRawX(int) method; simulate it pending future API * approval. */ private static float getRawX(final MotionEvent event, final int pointerIndex) { final float offset = event.getX() - event.getRawX(); return event.getX(pointerIndex) + offset; } /** * MotionEvent has no getRawY(int) method; simulate it pending future API * approval. */ private static float getRawY(final MotionEvent event, final int pointerIndex) { final float offset = event.getY() - event.getRawY(); return event.getY(pointerIndex) + offset; } 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; public ScaleGestureDetector(final Context context, final OnScaleGestureListener listener) { final ViewConfiguration config = ViewConfiguration.get(context); this.mContext = context; this.mListener = listener; this.mEdgeSlop = config.getScaledEdgeSlop(); } /** * Return the current distance between the two pointers forming the gesture * in progress. * * @return Distance between pointers in pixels. */ public float getCurrentSpan() { if (this.mCurrLen == -1) { final float cvx = this.mCurrFingerDiffX; final float cvy = this.mCurrFingerDiffY; this.mCurrLen = FloatMath.sqrt((cvx * cvx) + (cvy * cvy)); } return this.mCurrLen; } /** * Return the event time of the current event being processed. * * @return Current event time in milliseconds. */ public long getEventTime() { return this.mCurrEvent.getEventTime(); } /** * 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 this.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 this.mFocusY; } /** * Return the previous distance between the two pointers forming the gesture * in progress. * * @return Previous distance between pointers in pixels. */ public float getPreviousSpan() { if (this.mPrevLen == -1) { final float pvx = this.mPrevFingerDiffX; final float pvy = this.mPrevFingerDiffY; this.mPrevLen = FloatMath.sqrt((pvx * pvx) + (pvy * pvy)); } return this.mPrevLen; } /** * 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 (this.mScaleFactor == -1) { this.mScaleFactor = getCurrentSpan() / getPreviousSpan(); } return this.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 this.mTimeDelta; } /** * 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 this.mGestureInProgress; } public boolean onTouchEvent(final MotionEvent event) { final int action = event.getAction(); final boolean handled = true; if (!this.mGestureInProgress) { switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_POINTER_DOWN: { // We have a new multi-finger gesture // as orientation can change, query the metrics in touch down final DisplayMetrics metrics = this.mContext.getResources() .getDisplayMetrics(); this.mRightSlopEdge = metrics.widthPixels - this.mEdgeSlop; this.mBottomSlopEdge = metrics.heightPixels - this.mEdgeSlop; // Be paranoid in case we missed an event reset(); this.mPrevEvent = MotionEvent.obtain(event); this.mTimeDelta = 0; 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 = this.mEdgeSlop; final float rightSlop = this.mRightSlopEdge; final float bottomSlop = this.mBottomSlopEdge; final float x0 = event.getRawX(); final float y0 = event.getRawY(); final float x1 = ScaleGestureDetector.getRawX(event, 1); final float y1 = ScaleGestureDetector.getRawY(event, 1); final boolean p0sloppy = (x0 < edgeSlop) || (y0 < edgeSlop) || (x0 > rightSlop) || (y0 > bottomSlop); final boolean p1sloppy = (x1 < edgeSlop) || (y1 < edgeSlop) || (x1 > rightSlop) || (y1 > bottomSlop); if (p0sloppy && p1sloppy) { this.mFocusX = -1; this.mFocusY = -1; this.mSloppyGesture = true; } else if (p0sloppy) { this.mFocusX = event.getX(1); this.mFocusY = event.getY(1); this.mSloppyGesture = true; } else if (p1sloppy) { this.mFocusX = event.getX(0); this.mFocusY = event.getY(0); this.mSloppyGesture = true; } else { this.mGestureInProgress = this.mListener.onScaleBegin(this); } } break; case MotionEvent.ACTION_MOVE: if (this.mSloppyGesture) { // Initiate sloppy gestures if we've moved outside of the // slop area. final float edgeSlop = this.mEdgeSlop; final float rightSlop = this.mRightSlopEdge; final float bottomSlop = this.mBottomSlopEdge; final float x0 = event.getRawX(); final float y0 = event.getRawY(); final float x1 = ScaleGestureDetector.getRawX(event, 1); final float y1 = ScaleGestureDetector.getRawY(event, 1); final boolean p0sloppy = (x0 < edgeSlop) || (y0 < edgeSlop) || (x0 > rightSlop) || (y0 > bottomSlop); final boolean p1sloppy = (x1 < edgeSlop) || (y1 < edgeSlop) || (x1 > rightSlop) || (y1 > bottomSlop); if (p0sloppy && p1sloppy) { this.mFocusX = -1; this.mFocusY = -1; } else if (p0sloppy) { this.mFocusX = event.getX(1); this.mFocusY = event.getY(1); } else if (p1sloppy) { this.mFocusX = event.getX(0); this.mFocusY = event.getY(0); } else { this.mSloppyGesture = false; this.mGestureInProgress = this.mListener .onScaleBegin(this); } } break; case MotionEvent.ACTION_POINTER_UP: if (this.mSloppyGesture) { // Set focus point to the remaining finger final int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0; this.mFocusX = event.getX(id); this.mFocusY = event.getY(id); } break; } } else { // Transform gesture in progress - attempt to handle it switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_POINTER_UP: // Gesture ended setContext(event); // Set focus point to the remaining finger final int id = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0; this.mFocusX = event.getX(id); this.mFocusY = event.getY(id); if (!this.mSloppyGesture) { this.mListener.onScaleEnd(this); } reset(); break; case MotionEvent.ACTION_CANCEL: if (!this.mSloppyGesture) { this.mListener.onScaleEnd(this); } 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 ((this.mCurrPressure / this.mPrevPressure) > ScaleGestureDetector.PRESSURE_THRESHOLD) { final boolean updatePrevious = this.mListener.onScale(this); if (updatePrevious) { this.mPrevEvent.recycle(); this.mPrevEvent = MotionEvent.obtain(event); } } break; } } return handled; } private void reset() { if (this.mPrevEvent != null) { this.mPrevEvent.recycle(); this.mPrevEvent = null; } if (this.mCurrEvent != null) { this.mCurrEvent.recycle(); this.mCurrEvent = null; } this.mSloppyGesture = false; this.mGestureInProgress = false; } private void setContext(final MotionEvent curr) { if (this.mCurrEvent != null) { this.mCurrEvent.recycle(); } this.mCurrEvent = MotionEvent.obtain(curr); this.mCurrLen = -1; this.mPrevLen = -1; this.mScaleFactor = -1; final MotionEvent prev = this.mPrevEvent; final float px0 = prev.getX(0); final float py0 = prev.getY(0); final float px1 = prev.getX(1); final float py1 = prev.getY(1); final float cx0 = curr.getX(0); final float cy0 = curr.getY(0); final float cx1 = curr.getX(1); final float cy1 = curr.getY(1); final float pvx = px1 - px0; final float pvy = py1 - py0; final float cvx = cx1 - cx0; final float cvy = cy1 - cy0; this.mPrevFingerDiffX = pvx; this.mPrevFingerDiffY = pvy; this.mCurrFingerDiffX = cvx; this.mCurrFingerDiffY = cvy; this.mFocusX = cx0 + (cvx * 0.5f); this.mFocusY = cy0 + (cvy * 0.5f); this.mTimeDelta = curr.getEventTime() - prev.getEventTime(); this.mCurrPressure = curr.getPressure(0) + curr.getPressure(1); this.mPrevPressure = prev.getPressure(0) + prev.getPressure(1); } }