Home | History | Annotate | Download | only in chromoting
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.chromoting;
      6 
      7 import android.content.Context;
      8 import android.view.MotionEvent;
      9 import android.view.ViewConfiguration;
     10 
     11 /**
     12  * Helper class for disambiguating whether to treat a two-finger gesture as a swipe or a pinch.
     13  * Initially, the status will be unknown, until the fingers have moved sufficiently far to
     14  * determine the intent.
     15  */
     16 public class SwipePinchDetector {
     17     /** Current state of the gesture. */
     18     private enum State {
     19         UNKNOWN,
     20         SWIPE,
     21         PINCH
     22     }
     23     private State mState = State.UNKNOWN;
     24 
     25     /** Initial coordinates of the two pointers in the current gesture. */
     26     private float mFirstX0;
     27     private float mFirstY0;
     28     private float mFirstX1;
     29     private float mFirstY1;
     30 
     31     /**
     32      * The initial coordinates above are valid when this flag is set. Used to determine whether a
     33      * MotionEvent's pointer coordinates are the first ones of the gesture.
     34      */
     35     private boolean mInGesture = false;
     36 
     37     /**
     38      * Threshold squared-distance, in pixels, to use for motion-detection.
     39      */
     40     private int mTouchSlopSquare;
     41 
     42     private void reset() {
     43         mState = State.UNKNOWN;
     44         mInGesture = false;
     45     }
     46 
     47     /** Construct a new detector, using the context to determine movement thresholds. */
     48     public SwipePinchDetector(Context context) {
     49         ViewConfiguration config = ViewConfiguration.get(context);
     50         int touchSlop = config.getScaledTouchSlop();
     51         mTouchSlopSquare = touchSlop * touchSlop;
     52     }
     53 
     54     /** Returns whether a swipe is in progress. */
     55     public boolean isSwiping() {
     56         return mState == State.SWIPE;
     57     }
     58 
     59     /** Returns whether a pinch is in progress. */
     60     public boolean isPinching() {
     61         return mState == State.PINCH;
     62     }
     63 
     64     /**
     65      * Analyzes the touch event to determine whether the user is swiping or pinching. Only
     66      * motion events with 2 pointers are considered here. Once the gesture is determined to be a
     67      * swipe or a pinch, further 2-finger motion-events will be ignored. When a different event is
     68      * passed in (motion event with != 2 pointers, or some other event type), this object will
     69      * revert back to the original UNKNOWN state.
     70      */
     71     public void onTouchEvent(MotionEvent event) {
     72         if (event.getPointerCount() != 2) {
     73             reset();
     74             return;
     75         }
     76 
     77         // Only MOVE or DOWN events are considered - all other events should finish any current
     78         // gesture and reset the detector. In addition, a DOWN event should reset the detector,
     79         // since it signals the start of the gesture. If the events are consistent, a DOWN event
     80         // will occur at the start of the gesture, but this implementation tries to cope in case
     81         // the first event is MOVE rather than DOWN.
     82         int action = event.getActionMasked();
     83         if (action != MotionEvent.ACTION_MOVE) {
     84             reset();
     85             if (action != MotionEvent.ACTION_POINTER_DOWN) {
     86                 return;
     87             }
     88         }
     89 
     90         // If the gesture is known, there is no need for further processing - the state should
     91         // remain the same until the gesture is complete, as tested above.
     92         if (mState != State.UNKNOWN) {
     93             return;
     94         }
     95 
     96         float currentX0 = event.getX(0);
     97         float currentY0 = event.getY(0);
     98         float currentX1 = event.getX(1);
     99         float currentY1 = event.getY(1);
    100         if (!mInGesture) {
    101             // This is the first event of the gesture, so store the pointer coordinates.
    102             mFirstX0 = currentX0;
    103             mFirstY0 = currentY0;
    104             mFirstX1 = currentX1;
    105             mFirstY1 = currentY1;
    106             mInGesture = true;
    107             return;
    108         }
    109 
    110         float deltaX0 = currentX0 - mFirstX0;
    111         float deltaY0 = currentY0 - mFirstY0;
    112         float deltaX1 = currentX1 - mFirstX1;
    113         float deltaY1 = currentY1 - mFirstY1;
    114 
    115         float squaredDistance0 = deltaX0 * deltaX0 + deltaY0 * deltaY0;
    116         float squaredDistance1 = deltaX1 * deltaX1 + deltaY1 * deltaY1;
    117 
    118 
    119         // If both fingers have moved beyond the touch-slop, it is safe to recognize the gesture.
    120         // However, one finger might be held stationary whilst the other finger is moved a long
    121         // distance. In this case, it is preferable to trigger a PINCH. This should be detected
    122         // soon enough to avoid triggering a sudden large change in the zoom level, but not so
    123         // soon that SWIPE never gets triggered.
    124 
    125         // Threshold level for triggering the PINCH gesture if one finger is stationary. This
    126         // cannot be equal to the touch-slop, because in that case, SWIPE would rarely be detected.
    127         // One finger would usually leave the touch-slop radius slightly before the other finger,
    128         // triggering a PINCH as described above. A larger radius gives an opportunity for
    129         // SWIPE to be detected. Doubling the radius is an arbitrary choice that works well.
    130         int pinchThresholdSquare = 4 * mTouchSlopSquare;
    131 
    132         boolean finger0Moved = squaredDistance0 > mTouchSlopSquare;
    133         boolean finger1Moved = squaredDistance1 > mTouchSlopSquare;
    134 
    135         if (!finger0Moved && !finger1Moved) {
    136             return;
    137         }
    138 
    139         if (finger0Moved && !finger1Moved) {
    140             if (squaredDistance0 > pinchThresholdSquare) {
    141                 mState = State.PINCH;
    142             }
    143             return;
    144         }
    145 
    146         if (!finger0Moved && finger1Moved) {
    147             if (squaredDistance1 > pinchThresholdSquare) {
    148                 mState = State.PINCH;
    149             }
    150             return;
    151         }
    152 
    153         // Both fingers have moved, so determine SWIPE/PINCH status. If the fingers have moved in
    154         // the same direction, this is a SWIPE, otherwise it's a PINCH. This can be measured by
    155         // taking the scalar product of the direction vectors. This product is positive if the
    156         // vectors are pointing in the same direction, and negative if they're in opposite
    157         // directions.
    158         float scalarProduct = deltaX0 * deltaX1 + deltaY0 * deltaY1;
    159         mState = (scalarProduct > 0) ? State.SWIPE : State.PINCH;
    160     }
    161 }
    162