mobile/android/base/gfx/SimpleScaleGestureDetector.java

changeset 0
6474c204b198
     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 +

mercurial