mobile/android/base/gfx/SimpleScaleGestureDetector.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko.gfx;
michael@0 7
michael@0 8 import org.json.JSONException;
michael@0 9
michael@0 10 import android.graphics.PointF;
michael@0 11 import android.util.Log;
michael@0 12 import android.view.MotionEvent;
michael@0 13
michael@0 14 import java.util.LinkedList;
michael@0 15 import java.util.ListIterator;
michael@0 16 import java.util.Stack;
michael@0 17
michael@0 18 /**
michael@0 19 * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
michael@0 20 *
michael@0 21 * This gesture detector is more reliable than the built-in ScaleGestureDetector because:
michael@0 22 *
michael@0 23 * - It doesn't assume that pointer IDs are numbered 0 and 1.
michael@0 24 *
michael@0 25 * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some
michael@0 26 * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many
michael@0 27 * pointers are down, with disastrous results (bug 706684).
michael@0 28 *
michael@0 29 * - Cancelling a zoom into a pan is handled correctly.
michael@0 30 *
michael@0 31 * - Starting with three or more fingers down, releasing fingers so that only two are down, and
michael@0 32 * then performing a scale gesture is handled correctly.
michael@0 33 *
michael@0 34 * - It doesn't take pressure into account, which results in smoother scaling.
michael@0 35 */
michael@0 36 class SimpleScaleGestureDetector {
michael@0 37 private static final String LOGTAG = "GeckoSimpleScaleGestureDetector";
michael@0 38
michael@0 39 private SimpleScaleGestureListener mListener;
michael@0 40 private long mLastEventTime;
michael@0 41 private boolean mScaleResult;
michael@0 42
michael@0 43 /* Information about all pointers that are down. */
michael@0 44 private LinkedList<PointerInfo> mPointerInfo;
michael@0 45
michael@0 46 /** Creates a new gesture detector with the given listener. */
michael@0 47 SimpleScaleGestureDetector(SimpleScaleGestureListener listener) {
michael@0 48 mListener = listener;
michael@0 49 mPointerInfo = new LinkedList<PointerInfo>();
michael@0 50 }
michael@0 51
michael@0 52 /** Forward touch events to this function. */
michael@0 53 public void onTouchEvent(MotionEvent event) {
michael@0 54 switch (event.getAction() & MotionEvent.ACTION_MASK) {
michael@0 55 case MotionEvent.ACTION_DOWN:
michael@0 56 // If we get ACTION_DOWN while still tracking any pointers,
michael@0 57 // something is wrong. Cancel the current gesture and start over.
michael@0 58 if (getPointersDown() > 0)
michael@0 59 onTouchEnd(event);
michael@0 60 onTouchStart(event);
michael@0 61 break;
michael@0 62 case MotionEvent.ACTION_POINTER_DOWN:
michael@0 63 onTouchStart(event);
michael@0 64 break;
michael@0 65 case MotionEvent.ACTION_MOVE:
michael@0 66 onTouchMove(event);
michael@0 67 break;
michael@0 68 case MotionEvent.ACTION_POINTER_UP:
michael@0 69 case MotionEvent.ACTION_UP:
michael@0 70 case MotionEvent.ACTION_CANCEL:
michael@0 71 onTouchEnd(event);
michael@0 72 break;
michael@0 73 }
michael@0 74 }
michael@0 75
michael@0 76 private int getPointersDown() {
michael@0 77 return mPointerInfo.size();
michael@0 78 }
michael@0 79
michael@0 80 private int getActionIndex(MotionEvent event) {
michael@0 81 return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
michael@0 82 >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
michael@0 83 }
michael@0 84
michael@0 85 private void onTouchStart(MotionEvent event) {
michael@0 86 mLastEventTime = event.getEventTime();
michael@0 87 mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event)));
michael@0 88 if (getPointersDown() == 2) {
michael@0 89 sendScaleGesture(EventType.BEGIN);
michael@0 90 }
michael@0 91 }
michael@0 92
michael@0 93 private void onTouchMove(MotionEvent event) {
michael@0 94 mLastEventTime = event.getEventTime();
michael@0 95 for (int i = 0; i < event.getPointerCount(); i++) {
michael@0 96 PointerInfo pointerInfo = pointerInfoForEventIndex(event, i);
michael@0 97 if (pointerInfo != null) {
michael@0 98 pointerInfo.populate(event, i);
michael@0 99 }
michael@0 100 }
michael@0 101
michael@0 102 if (getPointersDown() == 2) {
michael@0 103 sendScaleGesture(EventType.CONTINUE);
michael@0 104 }
michael@0 105 }
michael@0 106
michael@0 107 private void onTouchEnd(MotionEvent event) {
michael@0 108 mLastEventTime = event.getEventTime();
michael@0 109
michael@0 110 int action = event.getAction() & MotionEvent.ACTION_MASK;
michael@0 111 boolean isCancel = (action == MotionEvent.ACTION_CANCEL ||
michael@0 112 action == MotionEvent.ACTION_DOWN);
michael@0 113
michael@0 114 int id = event.getPointerId(getActionIndex(event));
michael@0 115 ListIterator<PointerInfo> iterator = mPointerInfo.listIterator();
michael@0 116 while (iterator.hasNext()) {
michael@0 117 PointerInfo pointerInfo = iterator.next();
michael@0 118 if (!(isCancel || pointerInfo.getId() == id)) {
michael@0 119 continue;
michael@0 120 }
michael@0 121
michael@0 122 // One of the pointers we were tracking was lifted. Remove its info object from the
michael@0 123 // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this
michael@0 124 // ended the gesture.
michael@0 125 iterator.remove();
michael@0 126 pointerInfo.recycle();
michael@0 127 if (getPointersDown() == 1) {
michael@0 128 sendScaleGesture(EventType.END);
michael@0 129 }
michael@0 130 }
michael@0 131 }
michael@0 132
michael@0 133 /**
michael@0 134 * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only
michael@0 135 * one finger is down, returns the location of that finger.
michael@0 136 */
michael@0 137 public float getFocusX() {
michael@0 138 switch (getPointersDown()) {
michael@0 139 case 1:
michael@0 140 return mPointerInfo.getFirst().getCurrent().x;
michael@0 141 case 2:
michael@0 142 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
michael@0 143 return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f;
michael@0 144 }
michael@0 145
michael@0 146 Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
michael@0 147 return 0.0f;
michael@0 148 }
michael@0 149
michael@0 150 /**
michael@0 151 * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only
michael@0 152 * one finger is down, returns the location of that finger.
michael@0 153 */
michael@0 154 public float getFocusY() {
michael@0 155 switch (getPointersDown()) {
michael@0 156 case 1:
michael@0 157 return mPointerInfo.getFirst().getCurrent().y;
michael@0 158 case 2:
michael@0 159 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
michael@0 160 return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f;
michael@0 161 }
michael@0 162
michael@0 163 Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
michael@0 164 return 0.0f;
michael@0 165 }
michael@0 166
michael@0 167 /** Returns the most recent distance between the two pointers. */
michael@0 168 public float getCurrentSpan() {
michael@0 169 if (getPointersDown() != 2) {
michael@0 170 Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!");
michael@0 171 return 0.0f;
michael@0 172 }
michael@0 173
michael@0 174 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
michael@0 175 return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent());
michael@0 176 }
michael@0 177
michael@0 178 /** Returns the second most recent distance between the two pointers. */
michael@0 179 public float getPreviousSpan() {
michael@0 180 if (getPointersDown() != 2) {
michael@0 181 Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!");
michael@0 182 return 0.0f;
michael@0 183 }
michael@0 184
michael@0 185 PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
michael@0 186 PointF a = pointerA.getPrevious(), b = pointerB.getPrevious();
michael@0 187 if (a == null || b == null) {
michael@0 188 a = pointerA.getCurrent();
michael@0 189 b = pointerB.getCurrent();
michael@0 190 }
michael@0 191
michael@0 192 return PointUtils.distance(a, b);
michael@0 193 }
michael@0 194
michael@0 195 /** Returns the time of the last event related to the gesture. */
michael@0 196 public long getEventTime() {
michael@0 197 return mLastEventTime;
michael@0 198 }
michael@0 199
michael@0 200 /** Returns true if the scale gesture is in progress and false otherwise. */
michael@0 201 public boolean isInProgress() {
michael@0 202 return getPointersDown() == 2;
michael@0 203 }
michael@0 204
michael@0 205 /* Sends the requested scale gesture notification to the listener. */
michael@0 206 private void sendScaleGesture(EventType eventType) {
michael@0 207 switch (eventType) {
michael@0 208 case BEGIN:
michael@0 209 mScaleResult = mListener.onScaleBegin(this);
michael@0 210 break;
michael@0 211 case CONTINUE:
michael@0 212 if (mScaleResult) {
michael@0 213 mListener.onScale(this);
michael@0 214 }
michael@0 215 break;
michael@0 216 case END:
michael@0 217 if (mScaleResult) {
michael@0 218 mListener.onScaleEnd(this);
michael@0 219 }
michael@0 220 break;
michael@0 221 }
michael@0 222 }
michael@0 223
michael@0 224 /*
michael@0 225 * Returns the pointer info corresponding to the given pointer index, or null if the pointer
michael@0 226 * isn't one that's being tracked.
michael@0 227 */
michael@0 228 private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) {
michael@0 229 int id = event.getPointerId(index);
michael@0 230 for (PointerInfo pointerInfo : mPointerInfo) {
michael@0 231 if (pointerInfo.getId() == id) {
michael@0 232 return pointerInfo;
michael@0 233 }
michael@0 234 }
michael@0 235 return null;
michael@0 236 }
michael@0 237
michael@0 238 private enum EventType {
michael@0 239 BEGIN,
michael@0 240 CONTINUE,
michael@0 241 END,
michael@0 242 }
michael@0 243
michael@0 244 /* Encapsulates information about one of the two fingers involved in the gesture. */
michael@0 245 private static class PointerInfo {
michael@0 246 /* A free list that recycles pointer info objects, to reduce GC pauses. */
michael@0 247 private static Stack<PointerInfo> sPointerInfoFreeList;
michael@0 248
michael@0 249 private int mId;
michael@0 250 private PointF mCurrent, mPrevious;
michael@0 251
michael@0 252 private PointerInfo() {
michael@0 253 // External users should use create() instead.
michael@0 254 }
michael@0 255
michael@0 256 /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
michael@0 257 public static PointerInfo create(MotionEvent event, int index) {
michael@0 258 if (sPointerInfoFreeList == null) {
michael@0 259 sPointerInfoFreeList = new Stack<PointerInfo>();
michael@0 260 }
michael@0 261
michael@0 262 PointerInfo pointerInfo;
michael@0 263 if (sPointerInfoFreeList.empty()) {
michael@0 264 pointerInfo = new PointerInfo();
michael@0 265 } else {
michael@0 266 pointerInfo = sPointerInfoFreeList.pop();
michael@0 267 }
michael@0 268
michael@0 269 pointerInfo.populate(event, index);
michael@0 270 return pointerInfo;
michael@0 271 }
michael@0 272
michael@0 273 /*
michael@0 274 * Fills in the fields of this instance from the given motion event and pointer index
michael@0 275 * within that event.
michael@0 276 */
michael@0 277 public void populate(MotionEvent event, int index) {
michael@0 278 mId = event.getPointerId(index);
michael@0 279 mPrevious = mCurrent;
michael@0 280 mCurrent = new PointF(event.getX(index), event.getY(index));
michael@0 281 }
michael@0 282
michael@0 283 public void recycle() {
michael@0 284 mId = -1;
michael@0 285 mPrevious = mCurrent = null;
michael@0 286 sPointerInfoFreeList.push(this);
michael@0 287 }
michael@0 288
michael@0 289 public int getId() { return mId; }
michael@0 290 public PointF getCurrent() { return mCurrent; }
michael@0 291 public PointF getPrevious() { return mPrevious; }
michael@0 292
michael@0 293 @Override
michael@0 294 public String toString() {
michael@0 295 if (mId == -1) {
michael@0 296 return "(up)";
michael@0 297 }
michael@0 298
michael@0 299 try {
michael@0 300 String prevString;
michael@0 301 if (mPrevious == null) {
michael@0 302 prevString = "n/a";
michael@0 303 } else {
michael@0 304 prevString = PointUtils.toJSON(mPrevious).toString();
michael@0 305 }
michael@0 306
michael@0 307 // The current position should always be non-null.
michael@0 308 String currentString = PointUtils.toJSON(mCurrent).toString();
michael@0 309 return "id=" + mId + " cur=" + currentString + " prev=" + prevString;
michael@0 310 } catch (JSONException e) {
michael@0 311 throw new RuntimeException(e);
michael@0 312 }
michael@0 313 }
michael@0 314 }
michael@0 315
michael@0 316 public static interface SimpleScaleGestureListener {
michael@0 317 public boolean onScale(SimpleScaleGestureDetector detector);
michael@0 318 public boolean onScaleBegin(SimpleScaleGestureDetector detector);
michael@0 319 public void onScaleEnd(SimpleScaleGestureDetector detector);
michael@0 320 }
michael@0 321 }
michael@0 322

mercurial