1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/SimpleScaleGestureDetector.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,322 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import org.json.JSONException; 1.12 + 1.13 +import android.graphics.PointF; 1.14 +import android.util.Log; 1.15 +import android.view.MotionEvent; 1.16 + 1.17 +import java.util.LinkedList; 1.18 +import java.util.ListIterator; 1.19 +import java.util.Stack; 1.20 + 1.21 +/** 1.22 + * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector. 1.23 + * 1.24 + * This gesture detector is more reliable than the built-in ScaleGestureDetector because: 1.25 + * 1.26 + * - It doesn't assume that pointer IDs are numbered 0 and 1. 1.27 + * 1.28 + * - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some 1.29 + * devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many 1.30 + * pointers are down, with disastrous results (bug 706684). 1.31 + * 1.32 + * - Cancelling a zoom into a pan is handled correctly. 1.33 + * 1.34 + * - Starting with three or more fingers down, releasing fingers so that only two are down, and 1.35 + * then performing a scale gesture is handled correctly. 1.36 + * 1.37 + * - It doesn't take pressure into account, which results in smoother scaling. 1.38 + */ 1.39 +class SimpleScaleGestureDetector { 1.40 + private static final String LOGTAG = "GeckoSimpleScaleGestureDetector"; 1.41 + 1.42 + private SimpleScaleGestureListener mListener; 1.43 + private long mLastEventTime; 1.44 + private boolean mScaleResult; 1.45 + 1.46 + /* Information about all pointers that are down. */ 1.47 + private LinkedList<PointerInfo> mPointerInfo; 1.48 + 1.49 + /** Creates a new gesture detector with the given listener. */ 1.50 + SimpleScaleGestureDetector(SimpleScaleGestureListener listener) { 1.51 + mListener = listener; 1.52 + mPointerInfo = new LinkedList<PointerInfo>(); 1.53 + } 1.54 + 1.55 + /** Forward touch events to this function. */ 1.56 + public void onTouchEvent(MotionEvent event) { 1.57 + switch (event.getAction() & MotionEvent.ACTION_MASK) { 1.58 + case MotionEvent.ACTION_DOWN: 1.59 + // If we get ACTION_DOWN while still tracking any pointers, 1.60 + // something is wrong. Cancel the current gesture and start over. 1.61 + if (getPointersDown() > 0) 1.62 + onTouchEnd(event); 1.63 + onTouchStart(event); 1.64 + break; 1.65 + case MotionEvent.ACTION_POINTER_DOWN: 1.66 + onTouchStart(event); 1.67 + break; 1.68 + case MotionEvent.ACTION_MOVE: 1.69 + onTouchMove(event); 1.70 + break; 1.71 + case MotionEvent.ACTION_POINTER_UP: 1.72 + case MotionEvent.ACTION_UP: 1.73 + case MotionEvent.ACTION_CANCEL: 1.74 + onTouchEnd(event); 1.75 + break; 1.76 + } 1.77 + } 1.78 + 1.79 + private int getPointersDown() { 1.80 + return mPointerInfo.size(); 1.81 + } 1.82 + 1.83 + private int getActionIndex(MotionEvent event) { 1.84 + return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) 1.85 + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 1.86 + } 1.87 + 1.88 + private void onTouchStart(MotionEvent event) { 1.89 + mLastEventTime = event.getEventTime(); 1.90 + mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event))); 1.91 + if (getPointersDown() == 2) { 1.92 + sendScaleGesture(EventType.BEGIN); 1.93 + } 1.94 + } 1.95 + 1.96 + private void onTouchMove(MotionEvent event) { 1.97 + mLastEventTime = event.getEventTime(); 1.98 + for (int i = 0; i < event.getPointerCount(); i++) { 1.99 + PointerInfo pointerInfo = pointerInfoForEventIndex(event, i); 1.100 + if (pointerInfo != null) { 1.101 + pointerInfo.populate(event, i); 1.102 + } 1.103 + } 1.104 + 1.105 + if (getPointersDown() == 2) { 1.106 + sendScaleGesture(EventType.CONTINUE); 1.107 + } 1.108 + } 1.109 + 1.110 + private void onTouchEnd(MotionEvent event) { 1.111 + mLastEventTime = event.getEventTime(); 1.112 + 1.113 + int action = event.getAction() & MotionEvent.ACTION_MASK; 1.114 + boolean isCancel = (action == MotionEvent.ACTION_CANCEL || 1.115 + action == MotionEvent.ACTION_DOWN); 1.116 + 1.117 + int id = event.getPointerId(getActionIndex(event)); 1.118 + ListIterator<PointerInfo> iterator = mPointerInfo.listIterator(); 1.119 + while (iterator.hasNext()) { 1.120 + PointerInfo pointerInfo = iterator.next(); 1.121 + if (!(isCancel || pointerInfo.getId() == id)) { 1.122 + continue; 1.123 + } 1.124 + 1.125 + // One of the pointers we were tracking was lifted. Remove its info object from the 1.126 + // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this 1.127 + // ended the gesture. 1.128 + iterator.remove(); 1.129 + pointerInfo.recycle(); 1.130 + if (getPointersDown() == 1) { 1.131 + sendScaleGesture(EventType.END); 1.132 + } 1.133 + } 1.134 + } 1.135 + 1.136 + /** 1.137 + * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only 1.138 + * one finger is down, returns the location of that finger. 1.139 + */ 1.140 + public float getFocusX() { 1.141 + switch (getPointersDown()) { 1.142 + case 1: 1.143 + return mPointerInfo.getFirst().getCurrent().x; 1.144 + case 2: 1.145 + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); 1.146 + return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f; 1.147 + } 1.148 + 1.149 + Log.e(LOGTAG, "No gesture taking place in getFocusX()!"); 1.150 + return 0.0f; 1.151 + } 1.152 + 1.153 + /** 1.154 + * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only 1.155 + * one finger is down, returns the location of that finger. 1.156 + */ 1.157 + public float getFocusY() { 1.158 + switch (getPointersDown()) { 1.159 + case 1: 1.160 + return mPointerInfo.getFirst().getCurrent().y; 1.161 + case 2: 1.162 + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); 1.163 + return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f; 1.164 + } 1.165 + 1.166 + Log.e(LOGTAG, "No gesture taking place in getFocusY()!"); 1.167 + return 0.0f; 1.168 + } 1.169 + 1.170 + /** Returns the most recent distance between the two pointers. */ 1.171 + public float getCurrentSpan() { 1.172 + if (getPointersDown() != 2) { 1.173 + Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!"); 1.174 + return 0.0f; 1.175 + } 1.176 + 1.177 + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); 1.178 + return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent()); 1.179 + } 1.180 + 1.181 + /** Returns the second most recent distance between the two pointers. */ 1.182 + public float getPreviousSpan() { 1.183 + if (getPointersDown() != 2) { 1.184 + Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!"); 1.185 + return 0.0f; 1.186 + } 1.187 + 1.188 + PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast(); 1.189 + PointF a = pointerA.getPrevious(), b = pointerB.getPrevious(); 1.190 + if (a == null || b == null) { 1.191 + a = pointerA.getCurrent(); 1.192 + b = pointerB.getCurrent(); 1.193 + } 1.194 + 1.195 + return PointUtils.distance(a, b); 1.196 + } 1.197 + 1.198 + /** Returns the time of the last event related to the gesture. */ 1.199 + public long getEventTime() { 1.200 + return mLastEventTime; 1.201 + } 1.202 + 1.203 + /** Returns true if the scale gesture is in progress and false otherwise. */ 1.204 + public boolean isInProgress() { 1.205 + return getPointersDown() == 2; 1.206 + } 1.207 + 1.208 + /* Sends the requested scale gesture notification to the listener. */ 1.209 + private void sendScaleGesture(EventType eventType) { 1.210 + switch (eventType) { 1.211 + case BEGIN: 1.212 + mScaleResult = mListener.onScaleBegin(this); 1.213 + break; 1.214 + case CONTINUE: 1.215 + if (mScaleResult) { 1.216 + mListener.onScale(this); 1.217 + } 1.218 + break; 1.219 + case END: 1.220 + if (mScaleResult) { 1.221 + mListener.onScaleEnd(this); 1.222 + } 1.223 + break; 1.224 + } 1.225 + } 1.226 + 1.227 + /* 1.228 + * Returns the pointer info corresponding to the given pointer index, or null if the pointer 1.229 + * isn't one that's being tracked. 1.230 + */ 1.231 + private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) { 1.232 + int id = event.getPointerId(index); 1.233 + for (PointerInfo pointerInfo : mPointerInfo) { 1.234 + if (pointerInfo.getId() == id) { 1.235 + return pointerInfo; 1.236 + } 1.237 + } 1.238 + return null; 1.239 + } 1.240 + 1.241 + private enum EventType { 1.242 + BEGIN, 1.243 + CONTINUE, 1.244 + END, 1.245 + } 1.246 + 1.247 + /* Encapsulates information about one of the two fingers involved in the gesture. */ 1.248 + private static class PointerInfo { 1.249 + /* A free list that recycles pointer info objects, to reduce GC pauses. */ 1.250 + private static Stack<PointerInfo> sPointerInfoFreeList; 1.251 + 1.252 + private int mId; 1.253 + private PointF mCurrent, mPrevious; 1.254 + 1.255 + private PointerInfo() { 1.256 + // External users should use create() instead. 1.257 + } 1.258 + 1.259 + /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */ 1.260 + public static PointerInfo create(MotionEvent event, int index) { 1.261 + if (sPointerInfoFreeList == null) { 1.262 + sPointerInfoFreeList = new Stack<PointerInfo>(); 1.263 + } 1.264 + 1.265 + PointerInfo pointerInfo; 1.266 + if (sPointerInfoFreeList.empty()) { 1.267 + pointerInfo = new PointerInfo(); 1.268 + } else { 1.269 + pointerInfo = sPointerInfoFreeList.pop(); 1.270 + } 1.271 + 1.272 + pointerInfo.populate(event, index); 1.273 + return pointerInfo; 1.274 + } 1.275 + 1.276 + /* 1.277 + * Fills in the fields of this instance from the given motion event and pointer index 1.278 + * within that event. 1.279 + */ 1.280 + public void populate(MotionEvent event, int index) { 1.281 + mId = event.getPointerId(index); 1.282 + mPrevious = mCurrent; 1.283 + mCurrent = new PointF(event.getX(index), event.getY(index)); 1.284 + } 1.285 + 1.286 + public void recycle() { 1.287 + mId = -1; 1.288 + mPrevious = mCurrent = null; 1.289 + sPointerInfoFreeList.push(this); 1.290 + } 1.291 + 1.292 + public int getId() { return mId; } 1.293 + public PointF getCurrent() { return mCurrent; } 1.294 + public PointF getPrevious() { return mPrevious; } 1.295 + 1.296 + @Override 1.297 + public String toString() { 1.298 + if (mId == -1) { 1.299 + return "(up)"; 1.300 + } 1.301 + 1.302 + try { 1.303 + String prevString; 1.304 + if (mPrevious == null) { 1.305 + prevString = "n/a"; 1.306 + } else { 1.307 + prevString = PointUtils.toJSON(mPrevious).toString(); 1.308 + } 1.309 + 1.310 + // The current position should always be non-null. 1.311 + String currentString = PointUtils.toJSON(mCurrent).toString(); 1.312 + return "id=" + mId + " cur=" + currentString + " prev=" + prevString; 1.313 + } catch (JSONException e) { 1.314 + throw new RuntimeException(e); 1.315 + } 1.316 + } 1.317 + } 1.318 + 1.319 + public static interface SimpleScaleGestureListener { 1.320 + public boolean onScale(SimpleScaleGestureDetector detector); 1.321 + public boolean onScaleBegin(SimpleScaleGestureDetector detector); 1.322 + public void onScaleEnd(SimpleScaleGestureDetector detector); 1.323 + } 1.324 +} 1.325 +