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