gecko-dev/mobile/android/base/gfx/SimpleScaleGestureDetector.java
Kartikaya Gupta 59c9b324f3 Bug 777468 - Move ownership of TouchEventHandler from LayerView to JavaPanZoomController. r=Cwiiis
This patch has a bunch of semi-independent changes that unfortunately
couldn't be split apart without introducing hacks to make stuff build
on the intermediate patches. The main changes are:
- Moving TouchEventHandler from LayerView to JavaPanZoomController
- Registering the touch interceptor on the LayerView rather than the
  TouchEventHandler
- Moving the Tab:HasTouchListener handler from GeckoApp to JPZC

The net effect of all of this is that the TouchEventHandler is hidden
behind the PanZoomController interface and not accessible to GeckoApp
or GeckoAppShell.

Additionally, some of the JPZC methods were renamed from onXXX to
handleXXX to maintain the convention that onXXX methods are "interface"
methods (i.e. exposed to arbitrary other code) whereas handleXXX
methods are private/package and should only be called in very specific
ways.
2013-02-08 09:13:09 -05:00

323 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.gfx;
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.
*/
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. */
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);
}
}