Avoid potential negative array index access to cached text.
[LibreOffice.git] / android / source / src / java / org / mozilla / gecko / gfx / SimpleScaleGestureDetector.java
blobe89015b5ed8c6ca70bf0c3a944e32ecefdd34cda
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;
18 /**
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) {
48 mListener = 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)
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;
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)) {
119 continue;
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);
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()) {
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;
146 Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
147 return 0.0f;
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()) {
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;
163 Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
164 return 0.0f;
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;
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()!");
182 return 0.0f;
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) {
207 switch (eventType) {
208 case BEGIN:
209 mScaleResult = mListener.onScaleBegin(this);
210 break;
211 case CONTINUE:
212 if (mScaleResult) {
213 mListener.onScale(this);
215 break;
216 case END:
217 if (mScaleResult) {
218 mListener.onScaleEnd(this);
220 break;
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) {
232 return pointerInfo;
235 return null;
238 private enum EventType {
239 BEGIN,
240 CONTINUE,
241 END,
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;
249 private int mId;
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();
265 } else {
266 pointerInfo = sPointerInfoFreeList.pop();
269 pointerInfo.populate(event, index);
270 return pointerInfo;
274 * Fills in the fields of this instance from the given motion event and pointer index
275 * within that event.
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() {
284 mId = -1;
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; }
293 @Override
294 public String toString() {
295 if (mId == -1) {
296 return "(up)";
299 try {
300 String prevString;
301 if (mPrevious == null) {
302 prevString = "n/a";
303 } else {
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);