|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko.gfx; |
|
7 |
|
8 import org.json.JSONException; |
|
9 |
|
10 import android.graphics.PointF; |
|
11 import android.util.Log; |
|
12 import android.view.MotionEvent; |
|
13 |
|
14 import java.util.LinkedList; |
|
15 import java.util.ListIterator; |
|
16 import java.util.Stack; |
|
17 |
|
18 /** |
|
19 * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector. |
|
20 * |
|
21 * This gesture detector is more reliable than the built-in ScaleGestureDetector because: |
|
22 * |
|
23 * - It doesn't assume that pointer IDs are numbered 0 and 1. |
|
24 * |
|
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). |
|
28 * |
|
29 * - Cancelling a zoom into a pan is handled correctly. |
|
30 * |
|
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. |
|
33 * |
|
34 * - It doesn't take pressure into account, which results in smoother scaling. |
|
35 */ |
|
36 class SimpleScaleGestureDetector { |
|
37 private static final String LOGTAG = "GeckoSimpleScaleGestureDetector"; |
|
38 |
|
39 private SimpleScaleGestureListener mListener; |
|
40 private long mLastEventTime; |
|
41 private boolean mScaleResult; |
|
42 |
|
43 /* Information about all pointers that are down. */ |
|
44 private LinkedList<PointerInfo> mPointerInfo; |
|
45 |
|
46 /** Creates a new gesture detector with the given listener. */ |
|
47 SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { |
|
48 mListener = listener; |
|
49 mPointerInfo = new LinkedList<PointerInfo>(); |
|
50 } |
|
51 |
|
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) |
|
59 onTouchEnd(event); |
|
60 onTouchStart(event); |
|
61 break; |
|
62 case MotionEvent.ACTION_POINTER_DOWN: |
|
63 onTouchStart(event); |
|
64 break; |
|
65 case MotionEvent.ACTION_MOVE: |
|
66 onTouchMove(event); |
|
67 break; |
|
68 case MotionEvent.ACTION_POINTER_UP: |
|
69 case MotionEvent.ACTION_UP: |
|
70 case MotionEvent.ACTION_CANCEL: |
|
71 onTouchEnd(event); |
|
72 break; |
|
73 } |
|
74 } |
|
75 |
|
76 private int getPointersDown() { |
|
77 return mPointerInfo.size(); |
|
78 } |
|
79 |
|
80 private int getActionIndex(MotionEvent event) { |
|
81 return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) |
|
82 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; |
|
83 } |
|
84 |
|
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); |
|
90 } |
|
91 } |
|
92 |
|
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); |
|
99 } |
|
100 } |
|
101 |
|
102 if (getPointersDown() == 2) { |
|
103 sendScaleGesture(EventType.CONTINUE); |
|
104 } |
|
105 } |
|
106 |
|
107 private void onTouchEnd(MotionEvent event) { |
|
108 mLastEventTime = event.getEventTime(); |
|
109 |
|
110 int action = event.getAction() & MotionEvent.ACTION_MASK; |
|
111 boolean isCancel = (action == MotionEvent.ACTION_CANCEL || |
|
112 action == MotionEvent.ACTION_DOWN); |
|
113 |
|
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)) { |
|
119 continue; |
|
120 } |
|
121 |
|
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. |
|
125 iterator.remove(); |
|
126 pointerInfo.recycle(); |
|
127 if (getPointersDown() == 1) { |
|
128 sendScaleGesture(EventType.END); |
|
129 } |
|
130 } |
|
131 } |
|
132 |
|
133 /** |
|
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. |
|
136 */ |
|
137 public float getFocusX() { |
|
138 switch (getPointersDown()) { |
|
139 case 1: |
|
140 return mPointerInfo.getFirst().getCurrent().x; |
|
141 case 2: |
|
142 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); |
|
143 return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f; |
|
144 } |
|
145 |
|
146 Log.e(LOGTAG, "No gesture taking place in getFocusX()!"); |
|
147 return 0.0f; |
|
148 } |
|
149 |
|
150 /** |
|
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. |
|
153 */ |
|
154 public float getFocusY() { |
|
155 switch (getPointersDown()) { |
|
156 case 1: |
|
157 return mPointerInfo.getFirst().getCurrent().y; |
|
158 case 2: |
|
159 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); |
|
160 return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f; |
|
161 } |
|
162 |
|
163 Log.e(LOGTAG, "No gesture taking place in getFocusY()!"); |
|
164 return 0.0f; |
|
165 } |
|
166 |
|
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()!"); |
|
171 return 0.0f; |
|
172 } |
|
173 |
|
174 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); |
|
175 return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent()); |
|
176 } |
|
177 |
|
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()!"); |
|
182 return 0.0f; |
|
183 } |
|
184 |
|
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(); |
|
190 } |
|
191 |
|
192 return PointUtils.distance(a, b); |
|
193 } |
|
194 |
|
195 /** Returns the time of the last event related to the gesture. */ |
|
196 public long getEventTime() { |
|
197 return mLastEventTime; |
|
198 } |
|
199 |
|
200 /** Returns true if the scale gesture is in progress and false otherwise. */ |
|
201 public boolean isInProgress() { |
|
202 return getPointersDown() == 2; |
|
203 } |
|
204 |
|
205 /* Sends the requested scale gesture notification to the listener. */ |
|
206 private void sendScaleGesture(EventType eventType) { |
|
207 switch (eventType) { |
|
208 case BEGIN: |
|
209 mScaleResult = mListener.onScaleBegin(this); |
|
210 break; |
|
211 case CONTINUE: |
|
212 if (mScaleResult) { |
|
213 mListener.onScale(this); |
|
214 } |
|
215 break; |
|
216 case END: |
|
217 if (mScaleResult) { |
|
218 mListener.onScaleEnd(this); |
|
219 } |
|
220 break; |
|
221 } |
|
222 } |
|
223 |
|
224 /* |
|
225 * Returns the pointer info corresponding to the given pointer index, or null if the pointer |
|
226 * isn't one that's being tracked. |
|
227 */ |
|
228 private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) { |
|
229 int id = event.getPointerId(index); |
|
230 for (PointerInfo pointerInfo : mPointerInfo) { |
|
231 if (pointerInfo.getId() == id) { |
|
232 return pointerInfo; |
|
233 } |
|
234 } |
|
235 return null; |
|
236 } |
|
237 |
|
238 private enum EventType { |
|
239 BEGIN, |
|
240 CONTINUE, |
|
241 END, |
|
242 } |
|
243 |
|
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; |
|
248 |
|
249 private int mId; |
|
250 private PointF mCurrent, mPrevious; |
|
251 |
|
252 private PointerInfo() { |
|
253 // External users should use create() instead. |
|
254 } |
|
255 |
|
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>(); |
|
260 } |
|
261 |
|
262 PointerInfo pointerInfo; |
|
263 if (sPointerInfoFreeList.empty()) { |
|
264 pointerInfo = new PointerInfo(); |
|
265 } else { |
|
266 pointerInfo = sPointerInfoFreeList.pop(); |
|
267 } |
|
268 |
|
269 pointerInfo.populate(event, index); |
|
270 return pointerInfo; |
|
271 } |
|
272 |
|
273 /* |
|
274 * Fills in the fields of this instance from the given motion event and pointer index |
|
275 * within that event. |
|
276 */ |
|
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)); |
|
281 } |
|
282 |
|
283 public void recycle() { |
|
284 mId = -1; |
|
285 mPrevious = mCurrent = null; |
|
286 sPointerInfoFreeList.push(this); |
|
287 } |
|
288 |
|
289 public int getId() { return mId; } |
|
290 public PointF getCurrent() { return mCurrent; } |
|
291 public PointF getPrevious() { return mPrevious; } |
|
292 |
|
293 @Override |
|
294 public String toString() { |
|
295 if (mId == -1) { |
|
296 return "(up)"; |
|
297 } |
|
298 |
|
299 try { |
|
300 String prevString; |
|
301 if (mPrevious == null) { |
|
302 prevString = "n/a"; |
|
303 } else { |
|
304 prevString = PointUtils.toJSON(mPrevious).toString(); |
|
305 } |
|
306 |
|
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); |
|
312 } |
|
313 } |
|
314 } |
|
315 |
|
316 public static interface SimpleScaleGestureListener { |
|
317 public boolean onScale(SimpleScaleGestureDetector detector); |
|
318 public boolean onScaleBegin(SimpleScaleGestureDetector detector); |
|
319 public void onScaleEnd(SimpleScaleGestureDetector detector); |
|
320 } |
|
321 } |
|
322 |