mobile/android/base/gfx/SimpleScaleGestureDetector.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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/. */
     6 package org.mozilla.gecko.gfx;
     8 import org.json.JSONException;
    10 import android.graphics.PointF;
    11 import android.util.Log;
    12 import android.view.MotionEvent;
    14 import java.util.LinkedList;
    15 import java.util.ListIterator;
    16 import java.util.Stack;
    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 class SimpleScaleGestureDetector {
    37     private static final String LOGTAG = "GeckoSimpleScaleGestureDetector";
    39     private SimpleScaleGestureListener mListener;
    40     private long mLastEventTime;
    41     private boolean mScaleResult;
    43     /* Information about all pointers that are down. */
    44     private LinkedList<PointerInfo> mPointerInfo;
    46     /** Creates a new gesture detector with the given listener. */
    47     SimpleScaleGestureDetector(SimpleScaleGestureListener listener) {
    48         mListener = listener;
    49         mPointerInfo = new LinkedList<PointerInfo>();
    50     }
    52     /** Forward touch events to this function. */
    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     }
    76     private int getPointersDown() {
    77         return mPointerInfo.size();
    78     }
    80     private int getActionIndex(MotionEvent event) {
    81         return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
    82             >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
    83     }
    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     }
    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         }
   102         if (getPointersDown() == 2) {
   103             sendScaleGesture(EventType.CONTINUE);
   104         }
   105     }
   107     private void onTouchEnd(MotionEvent event) {
   108         mLastEventTime = event.getEventTime();
   110         int action = event.getAction() & MotionEvent.ACTION_MASK;
   111         boolean isCancel = (action == MotionEvent.ACTION_CANCEL ||
   112                             action == MotionEvent.ACTION_DOWN);
   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             }
   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     }
   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      */
   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         }
   146         Log.e(LOGTAG, "No gesture taking place in getFocusX()!");
   147         return 0.0f;
   148     }
   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      */
   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         }
   163         Log.e(LOGTAG, "No gesture taking place in getFocusY()!");
   164         return 0.0f;
   165     }
   167     /** Returns the most recent distance between the two pointers. */
   168     public float getCurrentSpan() {
   169         if (getPointersDown() != 2) {
   170             Log.e(LOGTAG, "No gesture taking place in getCurrentSpan()!");
   171             return 0.0f;
   172         }
   174         PointerInfo pointerA = mPointerInfo.getFirst(), pointerB = mPointerInfo.getLast();
   175         return PointUtils.distance(pointerA.getCurrent(), pointerB.getCurrent());
   176     }
   178     /** Returns the second most recent distance between the two pointers. */
   179     public float getPreviousSpan() {
   180         if (getPointersDown() != 2) {
   181             Log.e(LOGTAG, "No gesture taking place in getPreviousSpan()!");
   182             return 0.0f;
   183         }
   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         }
   192         return PointUtils.distance(a, b);
   193     }
   195     /** Returns the time of the last event related to the gesture. */
   196     public long getEventTime() {
   197         return mLastEventTime;
   198     }
   200     /** Returns true if the scale gesture is in progress and false otherwise. */
   201     public boolean isInProgress() {
   202         return getPointersDown() == 2;
   203     }
   205     /* Sends the requested scale gesture notification to the listener. */
   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     }
   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      */
   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     }
   238     private enum EventType {
   239         BEGIN,
   240         CONTINUE,
   241         END,
   242     }
   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;
   249         private int mId;
   250         private PointF mCurrent, mPrevious;
   252         private PointerInfo() {
   253             // External users should use create() instead.
   254         }
   256         /* Creates or recycles a new PointerInfo instance from an event and a pointer index. */
   257         public static PointerInfo create(MotionEvent event, int index) {
   258             if (sPointerInfoFreeList == null) {
   259                 sPointerInfoFreeList = new Stack<PointerInfo>();
   260             }
   262             PointerInfo pointerInfo;
   263             if (sPointerInfoFreeList.empty()) {
   264                 pointerInfo = new PointerInfo();
   265             } else {
   266                 pointerInfo = sPointerInfoFreeList.pop();
   267             }
   269             pointerInfo.populate(event, index);
   270             return pointerInfo;
   271         }
   273         /*
   274          * Fills in the fields of this instance from the given motion event and pointer index
   275          * within that event.
   276          */
   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         }
   283         public void recycle() {
   284             mId = -1;
   285             mPrevious = mCurrent = null;
   286             sPointerInfoFreeList.push(this);
   287         }
   289         public int getId() { return mId; }
   290         public PointF getCurrent() { return mCurrent; }
   291         public PointF getPrevious() { return mPrevious; }
   293         @Override
   294         public String toString() {
   295             if (mId == -1) {
   296                 return "(up)";
   297             }
   299             try {
   300                 String prevString;
   301                 if (mPrevious == null) {
   302                     prevString = "n/a";
   303                 } else {
   304                     prevString = PointUtils.toJSON(mPrevious).toString();
   305                 }
   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     }
   316     public static interface SimpleScaleGestureListener {
   317         public boolean onScale(SimpleScaleGestureDetector detector);
   318         public boolean onScaleBegin(SimpleScaleGestureDetector detector);
   319         public void onScaleEnd(SimpleScaleGestureDetector detector);
   320     }
   321 }

mercurial