• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
reset()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. */
SwipePinchDetector(Context context)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. */
isSwiping()55     public boolean isSwiping() {
56         return mState == State.SWIPE;
57     }
58 
59     /** Returns whether a pinch is in progress. */
isPinching()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      */
onTouchEvent(MotionEvent event)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