1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org
.mozilla
.gecko
.gfx
;
8 import android
.graphics
.PointF
;
9 import android
.util
.Log
;
10 import android
.view
.MotionEvent
;
12 import org
.json
.JSONException
;
14 import java
.util
.LinkedList
;
15 import java
.util
.ListIterator
;
16 import java
.util
.Stack
;
19 * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
21 * This gesture detector is more reliable than the built-in ScaleGestureDetector because:
23 * - It doesn't assume that pointer IDs are numbered 0 and 1.
25 * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some
26 * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many
27 * pointers are down, with disastrous results (bug 706684).
29 * - Cancelling a zoom into a pan is handled correctly.
31 * - Starting with three or more fingers down, releasing fingers so that only two are down, and
32 * then performing a scale gesture is handled correctly.
34 * - It doesn't take pressure into account, which results in smoother scaling.
36 public class SimpleScaleGestureDetector
{
37 private static final String LOGTAG
= "ScaleGestureDetector";
39 private SimpleScaleGestureListener mListener
;
40 private long mLastEventTime
;
41 private boolean mScaleResult
;
43 /* Information about all pointers that are down. */
44 private LinkedList
<PointerInfo
> mPointerInfo
;
46 /** Creates a new gesture detector with the given listener. */
47 public SimpleScaleGestureDetector(SimpleScaleGestureListener listener
) {
49 mPointerInfo
= new LinkedList
<PointerInfo
>();
52 /** Forward touch events to this function. */
53 public void onTouchEvent(MotionEvent event
) {
54 switch (event
.getAction() & MotionEvent
.ACTION_MASK
) {
55 case MotionEvent
.ACTION_DOWN
:
56 // If we get ACTION_DOWN while still tracking any pointers,
57 // something is wrong. Cancel the current gesture and start over.
58 if (getPointersDown() > 0)
62 case MotionEvent
.ACTION_POINTER_DOWN
:
65 case MotionEvent
.ACTION_MOVE
:
68 case MotionEvent
.ACTION_POINTER_UP
:
69 case MotionEvent
.ACTION_UP
:
70 case MotionEvent
.ACTION_CANCEL
:
76 private int getPointersDown() {
77 return mPointerInfo
.size();
80 private int getActionIndex(MotionEvent event
) {
81 return (event
.getAction() & MotionEvent
.ACTION_POINTER_INDEX_MASK
)
82 >> MotionEvent
.ACTION_POINTER_INDEX_SHIFT
;
85 private void onTouchStart(MotionEvent event
) {
86 mLastEventTime
= event
.getEventTime();
87 mPointerInfo
.addFirst(PointerInfo
.create(event
, getActionIndex(event
)));
88 if (getPointersDown() == 2) {
89 sendScaleGesture(EventType
.BEGIN
);
93 private void onTouchMove(MotionEvent event
) {
94 mLastEventTime
= event
.getEventTime();
95 for (int i
= 0; i
< event
.getPointerCount(); i
++) {
96 PointerInfo pointerInfo
= pointerInfoForEventIndex(event
, i
);
97 if (pointerInfo
!= null) {
98 pointerInfo
.populate(event
, i
);
102 if (getPointersDown() == 2) {
103 sendScaleGesture(EventType
.CONTINUE
);
107 private void onTouchEnd(MotionEvent event
) {
108 mLastEventTime
= event
.getEventTime();
110 int action
= event
.getAction() & MotionEvent
.ACTION_MASK
;
111 boolean isCancel
= (action
== MotionEvent
.ACTION_CANCEL
||
112 action
== MotionEvent
.ACTION_DOWN
);
114 int id
= event
.getPointerId(getActionIndex(event
));
115 ListIterator
<PointerInfo
> iterator
= mPointerInfo
.listIterator();
116 while (iterator
.hasNext()) {
117 PointerInfo pointerInfo
= iterator
.next();
118 if (!(isCancel
|| pointerInfo
.getId() == id
)) {
122 // One of the pointers we were tracking was lifted. Remove its info object from the
123 // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this
124 // ended the gesture.
126 pointerInfo
.recycle();
127 if (getPointersDown() == 1) {
128 sendScaleGesture(EventType
.END
);
134 * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only
135 * one finger is down, returns the location of that finger.
137 public float getFocusX() {
138 switch (getPointersDown()) {
140 return mPointerInfo
.getFirst().getCurrent().x
;
142 PointerInfo pointerA
= mPointerInfo
.getFirst(), pointerB
= mPointerInfo
.getLast();
143 return (pointerA
.getCurrent().x
+ pointerB
.getCurrent().x
) / 2.0f
;
146 Log
.e(LOGTAG
, "No gesture taking place in getFocusX()!");
151 * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only
152 * one finger is down, returns the location of that finger.
154 public float getFocusY() {
155 switch (getPointersDown()) {
157 return mPointerInfo
.getFirst().getCurrent().y
;
159 PointerInfo pointerA
= mPointerInfo
.getFirst(), pointerB
= mPointerInfo
.getLast();
160 return (pointerA
.getCurrent().y
+ pointerB
.getCurrent().y
) / 2.0f
;
163 Log
.e(LOGTAG
, "No gesture taking place in getFocusY()!");
167 /** Returns the most recent distance between the two pointers. */
168 public float getCurrentSpan() {
169 if (getPointersDown() != 2) {
170 Log
.e(LOGTAG
, "No gesture taking place in getCurrentSpan()!");
174 PointerInfo pointerA
= mPointerInfo
.getFirst(), pointerB
= mPointerInfo
.getLast();
175 return PointUtils
.distance(pointerA
.getCurrent(), pointerB
.getCurrent());
178 /** Returns the second most recent distance between the two pointers. */
179 public float getPreviousSpan() {
180 if (getPointersDown() != 2) {
181 Log
.e(LOGTAG
, "No gesture taking place in getPreviousSpan()!");
185 PointerInfo pointerA
= mPointerInfo
.getFirst(), pointerB
= mPointerInfo
.getLast();
186 PointF a
= pointerA
.getPrevious(), b
= pointerB
.getPrevious();
187 if (a
== null || b
== null) {
188 a
= pointerA
.getCurrent();
189 b
= pointerB
.getCurrent();
192 return PointUtils
.distance(a
, b
);
195 /** Returns the time of the last event related to the gesture. */
196 public long getEventTime() {
197 return mLastEventTime
;
200 /** Returns true if the scale gesture is in progress and false otherwise. */
201 public boolean isInProgress() {
202 return getPointersDown() == 2;
205 /* Sends the requested scale gesture notification to the listener. */
206 private void sendScaleGesture(EventType eventType
) {
209 mScaleResult
= mListener
.onScaleBegin(this);
213 mListener
.onScale(this);
218 mListener
.onScaleEnd(this);
225 * Returns the pointer info corresponding to the given pointer index, or null if the pointer
226 * isn't one that's being tracked.
228 private PointerInfo
pointerInfoForEventIndex(MotionEvent event
, int index
) {
229 int id
= event
.getPointerId(index
);
230 for (PointerInfo pointerInfo
: mPointerInfo
) {
231 if (pointerInfo
.getId() == id
) {
238 private enum EventType
{
244 /* Encapsulates information about one of the two fingers involved in the gesture. */
245 private static class PointerInfo
{
246 /* A free list that recycles pointer info objects, to reduce GC pauses. */
247 private static Stack
<PointerInfo
> sPointerInfoFreeList
;
250 private PointF mCurrent
, mPrevious
;
252 private PointerInfo() {
253 // External users should use create() instead.
256 /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
257 public static PointerInfo
create(MotionEvent event
, int index
) {
258 if (sPointerInfoFreeList
== null) {
259 sPointerInfoFreeList
= new Stack
<PointerInfo
>();
262 PointerInfo pointerInfo
;
263 if (sPointerInfoFreeList
.empty()) {
264 pointerInfo
= new PointerInfo();
266 pointerInfo
= sPointerInfoFreeList
.pop();
269 pointerInfo
.populate(event
, index
);
274 * Fills in the fields of this instance from the given motion event and pointer index
277 public void populate(MotionEvent event
, int index
) {
278 mId
= event
.getPointerId(index
);
279 mPrevious
= mCurrent
;
280 mCurrent
= new PointF(event
.getX(index
), event
.getY(index
));
283 public void recycle() {
285 mPrevious
= mCurrent
= null;
286 sPointerInfoFreeList
.push(this);
289 public int getId() { return mId
; }
290 public PointF
getCurrent() { return mCurrent
; }
291 public PointF
getPrevious() { return mPrevious
; }
294 public String
toString() {
301 if (mPrevious
== null) {
304 prevString
= PointUtils
.toJSON(mPrevious
).toString();
307 // The current position should always be non-null.
308 String currentString
= PointUtils
.toJSON(mCurrent
).toString();
309 return "id=" + mId
+ " cur=" + currentString
+ " prev=" + prevString
;
310 } catch (JSONException e
) {
311 throw new RuntimeException(e
);
316 public static interface SimpleScaleGestureListener
{
317 public boolean onScale(SimpleScaleGestureDetector detector
);
318 public boolean onScaleBegin(SimpleScaleGestureDetector detector
);
319 public void onScaleEnd(SimpleScaleGestureDetector detector
);