summaryrefslogtreecommitdiff
path: root/android/source/src/java/org/mozilla/gecko/gfx/SimpleScaleGestureDetector.java
blob: e89015b5ed8c6ca70bf0c3a944e32ecefdd34cda (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.gfx;

import android.graphics.PointF;
import android.util.Log;
import android.view.MotionEvent;

import org.json.JSONException;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Stack;

/**
 * A less buggy, and smoother, replacement for the built-in Android ScaleGestureDetector.
 *
 * This gesture detector is more reliable than the built-in ScaleGestureDetector because:
 *
 *   - It doesn't assume that pointer IDs are numbered 0 and 1.
 *
 *   - It doesn't attempt to correct for "slop" when resting one's hand on the device. On some
 *     devices (e.g. the Droid X) this can cause the ScaleGestureDetector to lose track of how many
 *     pointers are down, with disastrous results (bug 706684).
 *
 *   - Cancelling a zoom into a pan is handled correctly.
 *
 *   - Starting with three or more fingers down, releasing fingers so that only two are down, and
 *     then performing a scale gesture is handled correctly.
 *
 *   - It doesn't take pressure into account, which results in smoother scaling.
 */
public class SimpleScaleGestureDetector {
    private static final String LOGTAG = "ScaleGestureDetector";

    private SimpleScaleGestureListener mListener;
    private long mLastEventTime;
    private boolean mScaleResult;

    /* Information about all pointers that are down. */
    private LinkedList<PointerInfo> mPointerInfo;

    /** Creates a new gesture detector with the given listener. */
    public SimpleScaleGestureDetector(SimpleScaleGestureListener listener) {
        mListener = listener;
        mPointerInfo = new LinkedList<PointerInfo>();
    }

    /** Forward touch events to this function. */
    public void onTouchEvent(MotionEvent event) {
        switch (event.getAction() & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN:
            // If we get ACTION_DOWN while still tracking any pointers,
            // something is wrong.  Cancel the current gesture and start over.
            if (getPointersDown() > 0)
                onTouchEnd(event);
            onTouchStart(event);
            break;
        case MotionEvent.ACTION_POINTER_DOWN:
            onTouchStart(event);
            break;
        case MotionEvent.ACTION_MOVE:
            onTouchMove(event);
            break;
        case MotionEvent.ACTION_POINTER_UP:
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            onTouchEnd(event);
            break;
        }
    }

    private int getPointersDown() {
        return mPointerInfo.size();
    }

    private int getActionIndex(MotionEvent event) {
        return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
            >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
    }

    private void onTouchStart(MotionEvent event) {
        mLastEventTime = event.getEventTime();
        mPointerInfo.addFirst(PointerInfo.create(event, getActionIndex(event)));
        if (getPointersDown() == 2) {
            sendScaleGesture(EventType.BEGIN);
        }
    }

    private void onTouchMove(MotionEvent event) {
        mLastEventTime = event.getEventTime();
        for (int i = 0; i < event.getPointerCount(); i++) {
            PointerInfo pointerInfo = pointerInfoForEventIndex(event, i);
            if (pointerInfo != null) {
                pointerInfo.populate(event, i);
            }
        }

        if (getPointersDown() == 2) {
            sendScaleGesture(EventType.CONTINUE);
        }
    }

    private void onTouchEnd(MotionEvent event) {
        mLastEventTime = event.getEventTime();

        int action = event.getAction() & MotionEvent.ACTION_MASK;
        boolean isCancel = (action == MotionEvent.ACTION_CANCEL ||
                            action == MotionEvent.ACTION_DOWN);

        int id = event.getPointerId(getActionIndex(event));
        ListIterator<PointerInfo> iterator = mPointerInfo.listIterator();
        while (iterator.hasNext()) {
            PointerInfo pointerInfo = iterator.next();
            if (!(isCancel || pointerInfo.getId() == id)) {
                continue;
            }

            // One of the pointers we were tracking was lifted. Remove its info object from the
            // list, recycle it to avoid GC pauses, and send an onScaleEnd() notification if this
            // ended the gesture.
            iterator.remove();
            pointerInfo.recycle();
            if (getPointersDown() == 1) {
                sendScaleGesture(EventType.END);
            }
        }
    }

    /**
     * Returns the X coordinate of the focus location (the midpoint of the two fingers). If only
     * one finger is down, returns the location of that finger.
     */
    public float getFocusX() {
        switch (getPointersDown()) {
        case 1:
            return mPointerInfo.getFirst().getCurrent().x;
        case 2:
            PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
            return (pointerA.getCurrent().x + pointerB.getCurrent().x) / 2.0f;
        }

        Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
        return 0.0f;
    }

    /**
     * Returns the Y coordinate of the focus location (the midpoint of the two fingers). If only
     * one finger is down, returns the location of that finger.
     */
    public float getFocusY() {
        switch (getPointersDown()) {
        case 1:
            return mPointerInfo.getFirst().getCurrent().y;
        case 2:
            PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
            return (pointerA.getCurrent().y + pointerB.getCurrent().y) / 2.0f;
        }

        Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
        return 0.0f;
    }

    /** Returns the most recent distance between the two pointers. */
    public float getCurrentSpan() {
        if (getPointersDown() != 2) {
            Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!");
            return 0.0f;
        }

        PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
        return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent());
    }

    /** Returns the second most recent distance between the two pointers. */
    public float getPreviousSpan() {
        if (getPointersDown() != 2) {
            Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!");
            return 0.0f;
        }

        PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
        PointF a = pointerA.getPrevious(), b = pointerB.getPrevious();
        if (a == null || b == null) {
            a = pointerA.getCurrent();
            b = pointerB.getCurrent();
        }

        return PointUtils.distance(a, b);
    }

    /** Returns the time of the last event related to the gesture. */
    public long getEventTime() {
        return mLastEventTime;
    }

    /** Returns true if the scale gesture is in progress and false otherwise. */
    public boolean isInProgress() {
        return getPointersDown() == 2;
    }

    /* Sends the requested scale gesture notification to the listener. */
    private void sendScaleGesture(EventType eventType) {
        switch (eventType) {
        case BEGIN:
            mScaleResult = mListener.onScaleBegin(this);
            break;
        case CONTINUE:
            if (mScaleResult) {
                mListener.onScale(this);
            }
            break;
        case END:
            if (mScaleResult) {
                mListener.onScaleEnd(this);
            }
            break;
        }
    }

    /*
     * Returns the pointer info corresponding to the given pointer index, or null if the pointer
     * isn't one that's being tracked.
     */
    private PointerInfo pointerInfoForEventIndex(MotionEvent event, int index) {
        int id = event.getPointerId(index);
        for (PointerInfo pointerInfo : mPointerInfo) {
            if (pointerInfo.getId() == id) {
                return pointerInfo;
            }
        }
        return null;
    }

    private enum EventType {
        BEGIN,
        CONTINUE,
        END,
    }

    /* Encapsulates information about one of the two fingers involved in the gesture. */
    private static class PointerInfo {
        /* A free list that recycles pointer info objects, to reduce GC pauses. */
        private static Stack<PointerInfo> sPointerInfoFreeList;

        private int mId;
        private PointF mCurrent, mPrevious;

        private PointerInfo() {
            // External users should use create() instead.
        }

        /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
        public static PointerInfo create(MotionEvent event, int index) {
            if (sPointerInfoFreeList == null) {
                sPointerInfoFreeList = new Stack<PointerInfo>();
            }

            PointerInfo pointerInfo;
            if (sPointerInfoFreeList.empty()) {
                pointerInfo = new PointerInfo();
            } else {
                pointerInfo = sPointerInfoFreeList.pop();
            }

            pointerInfo.populate(event, index);
            return pointerInfo;
        }

        /*
         * Fills in the fields of this instance from the given motion event and pointer index
         * within that event.
         */
        public void populate(MotionEvent event, int index) {
            mId = event.getPointerId(index);
            mPrevious = mCurrent;
            mCurrent = new PointF(event.getX(index), event.getY(index));
        }

        public void recycle() {
            mId = -1;
            mPrevious = mCurrent = null;
            sPointerInfoFreeList.push(this);
        }

        public int getId() { return mId; }
        public PointF getCurrent() { return mCurrent; }
        public PointF getPrevious() { return mPrevious; }

        @Override
        public String toString() {
            if (mId == -1) {
                return "(up)";
            }

            try {
                String prevString;
                if (mPrevious == null) {
                    prevString = "n/a";
                } else {
                    prevString = PointUtils.toJSON(mPrevious).toString();
                }

                // The current position should always be non-null.
                String currentString = PointUtils.toJSON(mCurrent).toString();
                return "id=" + mId + " cur=" + currentString + " prev=" + prevString;
            } catch (JSONException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static interface SimpleScaleGestureListener {
        public boolean onScale(SimpleScaleGestureDetector detector);
        public boolean onScaleBegin(SimpleScaleGestureDetector detector);
        public void onScaleEnd(SimpleScaleGestureDetector detector);
    }
}