michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.json.JSONException; michael@0: michael@0: import android.graphics.PointF; michael@0: import android.util.Log; michael@0: import android.view.MotionEvent; michael@0: michael@0: import java.util.LinkedList; michael@0: import java.util.ListIterator; michael@0: import java.util.Stack; michael@0: michael@0: /** michael@0: * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector. michael@0: * michael@0: * This gesture detector is more reliable than the built-in ScaleGestureDetector because: michael@0: * michael@0: * - It doesn't assume that pointer IDs are numbered 0 and 1. michael@0: * michael@0: * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some michael@0: * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many michael@0: * pointers are down, with disastrous results (bug 706684). michael@0: * michael@0: * - Cancelling a zoom into a pan is handled correctly. michael@0: * michael@0: * - Starting with three or more fingers down, releasing fingers so that only two are down, and michael@0: * then performing a scale gesture is handled correctly. michael@0: * michael@0: * - It doesn't take pressure into account, which results in smoother scaling. michael@0: */ michael@0: class SimpleScaleGestureDetector { michael@0: private static final String LOGTAG = "GeckoSimpleScaleGestureDetector"; michael@0: michael@0: private SimpleScaleGestureListener mListener; michael@0: private long mLastEventTime; michael@0: private boolean mScaleResult; michael@0: michael@0: /* Information about all pointers that are down. */ michael@0: private LinkedList mPointerInfo; michael@0: michael@0: /** Creates a new gesture detector with the given listener. */ michael@0: SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { michael@0: mListener = listener; michael@0: mPointerInfo = new LinkedList(); michael@0: } michael@0: michael@0: /** Forward touch events to this function. */ michael@0: public void onTouchEvent(MotionEvent event) { michael@0: switch (event.getAction() & MotionEvent.ACTION_MASK) { michael@0: case MotionEvent.ACTION_DOWN: michael@0: // If we get ACTION_DOWN while still tracking any pointers, michael@0: // something is wrong. Cancel the current gesture and start over. michael@0: if (getPointersDown() > 0) michael@0: onTouchEnd(event); michael@0: onTouchStart(event); michael@0: break; michael@0: case MotionEvent.ACTION_POINTER_DOWN: michael@0: onTouchStart(event); michael@0: break; michael@0: case MotionEvent.ACTION_MOVE: michael@0: onTouchMove(event); michael@0: break; michael@0: case MotionEvent.ACTION_POINTER_UP: michael@0: case MotionEvent.ACTION_UP: michael@0: case MotionEvent.ACTION_CANCEL: michael@0: onTouchEnd(event); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: private int getPointersDown() { michael@0: return mPointerInfo.size(); michael@0: } michael@0: michael@0: private int getActionIndex(MotionEvent event) { michael@0: return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) michael@0: >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; michael@0: } michael@0: michael@0: private void onTouchStart(MotionEvent event) { michael@0: mLastEventTime = event.getEventTime(); michael@0: mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event))); michael@0: if (getPointersDown() == 2) { michael@0: sendScaleGesture(EventType.BEGIN); michael@0: } michael@0: } michael@0: michael@0: private void onTouchMove(MotionEvent event) { michael@0: mLastEventTime = event.getEventTime(); michael@0: for (int i = 0; i < event.getPointerCount(); i++) { michael@0: PointerInfo pointerInfo = pointerInfoForEventIndex(event, i); michael@0: if (pointerInfo != null) { michael@0: pointerInfo.populate(event, i); michael@0: } michael@0: } michael@0: michael@0: if (getPointersDown() == 2) { michael@0: sendScaleGesture(EventType.CONTINUE); michael@0: } michael@0: } michael@0: michael@0: private void onTouchEnd(MotionEvent event) { michael@0: mLastEventTime = event.getEventTime(); michael@0: michael@0: int action = event.getAction() & MotionEvent.ACTION_MASK; michael@0: boolean isCancel = (action == MotionEvent.ACTION_CANCEL || michael@0: action == MotionEvent.ACTION_DOWN); michael@0: michael@0: int id = event.getPointerId(getActionIndex(event)); michael@0: ListIterator iterator = mPointerInfo.listIterator(); michael@0: while (iterator.hasNext()) { michael@0: PointerInfo pointerInfo = iterator.next(); michael@0: if (!(isCancel || pointerInfo.getId() == id)) { michael@0: continue; michael@0: } michael@0: michael@0: // One of the pointers we were tracking was lifted. Remove its info object from the michael@0: // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this michael@0: // ended the gesture. michael@0: iterator.remove(); michael@0: pointerInfo.recycle(); michael@0: if (getPointersDown() == 1) { michael@0: sendScaleGesture(EventType.END); michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only michael@0: * one finger is down, returns the location of that finger. michael@0: */ michael@0: public float getFocusX() { michael@0: switch (getPointersDown()) { michael@0: case 1: michael@0: return mPointerInfo.getFirst().getCurrent().x; michael@0: case 2: michael@0: PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); michael@0: return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f; michael@0: } michael@0: michael@0: Log.e(LOGTAG, "No gesture taking place in getFocusX()!"); michael@0: return 0.0f; michael@0: } michael@0: michael@0: /** michael@0: * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only michael@0: * one finger is down, returns the location of that finger. michael@0: */ michael@0: public float getFocusY() { michael@0: switch (getPointersDown()) { michael@0: case 1: michael@0: return mPointerInfo.getFirst().getCurrent().y; michael@0: case 2: michael@0: PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); michael@0: return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f; michael@0: } michael@0: michael@0: Log.e(LOGTAG, "No gesture taking place in getFocusY()!"); michael@0: return 0.0f; michael@0: } michael@0: michael@0: /** Returns the most recent distance between the two pointers. */ michael@0: public float getCurrentSpan() { michael@0: if (getPointersDown() != 2) { michael@0: Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!"); michael@0: return 0.0f; michael@0: } michael@0: michael@0: PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); michael@0: return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent()); michael@0: } michael@0: michael@0: /** Returns the second most recent distance between the two pointers. */ michael@0: public float getPreviousSpan() { michael@0: if (getPointersDown() != 2) { michael@0: Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!"); michael@0: return 0.0f; michael@0: } michael@0: michael@0: PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); michael@0: PointF a = pointerA.getPrevious(), b = pointerB.getPrevious(); michael@0: if (a == null || b == null) { michael@0: a = pointerA.getCurrent(); michael@0: b = pointerB.getCurrent(); michael@0: } michael@0: michael@0: return PointUtils.distance(a, b); michael@0: } michael@0: michael@0: /** Returns the time of the last event related to the gesture. */ michael@0: public long getEventTime() { michael@0: return mLastEventTime; michael@0: } michael@0: michael@0: /** Returns true if the scale gesture is in progress and false otherwise. */ michael@0: public boolean isInProgress() { michael@0: return getPointersDown() == 2; michael@0: } michael@0: michael@0: /* Sends the requested scale gesture notification to the listener. */ michael@0: private void sendScaleGesture(EventType eventType) { michael@0: switch (eventType) { michael@0: case BEGIN: michael@0: mScaleResult = mListener.onScaleBegin(this); michael@0: break; michael@0: case CONTINUE: michael@0: if (mScaleResult) { michael@0: mListener.onScale(this); michael@0: } michael@0: break; michael@0: case END: michael@0: if (mScaleResult) { michael@0: mListener.onScaleEnd(this); michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * Returns the pointer info corresponding to the given pointer index, or null if the pointer michael@0: * isn't one that's being tracked. michael@0: */ michael@0: private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) { michael@0: int id = event.getPointerId(index); michael@0: for (PointerInfo pointerInfo : mPointerInfo) { michael@0: if (pointerInfo.getId() == id) { michael@0: return pointerInfo; michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: private enum EventType { michael@0: BEGIN, michael@0: CONTINUE, michael@0: END, michael@0: } michael@0: michael@0: /* Encapsulates information about one of the two fingers involved in the gesture. */ michael@0: private static class PointerInfo { michael@0: /* A free list that recycles pointer info objects, to reduce GC pauses. */ michael@0: private static Stack sPointerInfoFreeList; michael@0: michael@0: private int mId; michael@0: private PointF mCurrent, mPrevious; michael@0: michael@0: private PointerInfo() { michael@0: // External users should use create() instead. michael@0: } michael@0: michael@0: /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */ michael@0: public static PointerInfo create(MotionEvent event, int index) { michael@0: if (sPointerInfoFreeList == null) { michael@0: sPointerInfoFreeList = new Stack(); michael@0: } michael@0: michael@0: PointerInfo pointerInfo; michael@0: if (sPointerInfoFreeList.empty()) { michael@0: pointerInfo = new PointerInfo(); michael@0: } else { michael@0: pointerInfo = sPointerInfoFreeList.pop(); michael@0: } michael@0: michael@0: pointerInfo.populate(event, index); michael@0: return pointerInfo; michael@0: } michael@0: michael@0: /* michael@0: * Fills in the fields of this instance from the given motion event and pointer index michael@0: * within that event. michael@0: */ michael@0: public void populate(MotionEvent event, int index) { michael@0: mId = event.getPointerId(index); michael@0: mPrevious = mCurrent; michael@0: mCurrent = new PointF(event.getX(index), event.getY(index)); michael@0: } michael@0: michael@0: public void recycle() { michael@0: mId = -1; michael@0: mPrevious = mCurrent = null; michael@0: sPointerInfoFreeList.push(this); michael@0: } michael@0: michael@0: public int getId() { return mId; } michael@0: public PointF getCurrent() { return mCurrent; } michael@0: public PointF getPrevious() { return mPrevious; } michael@0: michael@0: @Override michael@0: public String toString() { michael@0: if (mId == -1) { michael@0: return "(up)"; michael@0: } michael@0: michael@0: try { michael@0: String prevString; michael@0: if (mPrevious == null) { michael@0: prevString = "n/a"; michael@0: } else { michael@0: prevString = PointUtils.toJSON(mPrevious).toString(); michael@0: } michael@0: michael@0: // The current position should always be non-null. michael@0: String currentString = PointUtils.toJSON(mCurrent).toString(); michael@0: return "id=" + mId + " cur=" + currentString + " prev=" + prevString; michael@0: } catch (JSONException e) { michael@0: throw new RuntimeException(e); michael@0: } michael@0: } michael@0: } michael@0: michael@0: public static interface SimpleScaleGestureListener { michael@0: public boolean onScale(SimpleScaleGestureDetector detector); michael@0: public boolean onScaleBegin(SimpleScaleGestureDetector detector); michael@0: public void onScaleEnd(SimpleScaleGestureDetector detector); michael@0: } michael@0: } michael@0: