gecko/mobile/android/base/ui/SimpleScaleGestureDetector.java

325 lines
11 KiB
Java

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