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 android.graphics.PointF; 9 import android.util.Log; 10 import android.view.MotionEvent; 11 12 import org.json.JSONException; 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 public 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. */ SimpleScaleGestureDetector(SimpleScaleGestureListener listener)47 public SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { 48 mListener = listener; 49 mPointerInfo = new LinkedList<PointerInfo>(); 50 } 51 52 /** Forward touch events to this function. */ onTouchEvent(MotionEvent event)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 getPointersDown()76 private int getPointersDown() { 77 return mPointerInfo.size(); 78 } 79 getActionIndex(MotionEvent event)80 private int getActionIndex(MotionEvent event) { 81 return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) 82 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 83 } 84 onTouchStart(MotionEvent event)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 onTouchMove(MotionEvent event)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 onTouchEnd(MotionEvent event)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 */ getFocusX()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 */ getFocusY()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. */ getCurrentSpan()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. */ getPreviousSpan()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. */ getEventTime()196 public long getEventTime() { 197 return mLastEventTime; 198 } 199 200 /** Returns true if the scale gesture is in progress and false otherwise. */ isInProgress()201 public boolean isInProgress() { 202 return getPointersDown() == 2; 203 } 204 205 /* Sends the requested scale gesture notification to the listener. */ sendScaleGesture(EventType eventType)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 */ pointerInfoForEventIndex(MotionEvent event, int index)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 PointerInfo()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. */ create(MotionEvent event, int 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 */ populate(MotionEvent event, int 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)); 281 } 282 recycle()283 public void recycle() { 284 mId = -1; 285 mPrevious = mCurrent = null; 286 sPointerInfoFreeList.push(this); 287 } 288 getId()289 public int getId() { return mId; } getCurrent()290 public PointF getCurrent() { return mCurrent; } getPrevious()291 public PointF getPrevious() { return mPrevious; } 292 293 @Override toString()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 { onScale(SimpleScaleGestureDetector detector)317 public boolean onScale(SimpleScaleGestureDetector detector); onScaleBegin(SimpleScaleGestureDetector detector)318 public boolean onScaleBegin(SimpleScaleGestureDetector detector); onScaleEnd(SimpleScaleGestureDetector detector)319 public void onScaleEnd(SimpleScaleGestureDetector detector); 320 } 321 } 322 323