| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chromoting; |
| |
| import android.content.Context; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| |
| /** |
| * Helper class for disambiguating whether to treat a two-finger gesture as a swipe or a pinch. |
| * Initially, the status will be unknown, until the fingers have moved sufficiently far to |
| * determine the intent. |
| */ |
| public class SwipePinchDetector { |
| /** Current state of the gesture. */ |
| private enum State { |
| UNKNOWN, |
| SWIPE, |
| PINCH |
| } |
| private State mState = State.UNKNOWN; |
| |
| /** Initial coordinates of the two pointers in the current gesture. */ |
| private float mFirstX0; |
| private float mFirstY0; |
| private float mFirstX1; |
| private float mFirstY1; |
| |
| /** |
| * The initial coordinates above are valid when this flag is set. Used to determine whether a |
| * MotionEvent's pointer coordinates are the first ones of the gesture. |
| */ |
| private boolean mInGesture = false; |
| |
| /** |
| * Threshold squared-distance, in pixels, to use for motion-detection. |
| */ |
| private int mTouchSlopSquare; |
| |
| private void reset() { |
| mState = State.UNKNOWN; |
| mInGesture = false; |
| } |
| |
| /** Construct a new detector, using the context to determine movement thresholds. */ |
| public SwipePinchDetector(Context context) { |
| ViewConfiguration config = ViewConfiguration.get(context); |
| int touchSlop = config.getScaledTouchSlop(); |
| mTouchSlopSquare = touchSlop * touchSlop; |
| } |
| |
| /** Returns whether a swipe is in progress. */ |
| public boolean isSwiping() { |
| return mState == State.SWIPE; |
| } |
| |
| /** Returns whether a pinch is in progress. */ |
| public boolean isPinching() { |
| return mState == State.PINCH; |
| } |
| |
| /** |
| * Analyzes the touch event to determine whether the user is swiping or pinching. Only |
| * motion events with 2 pointers are considered here. Once the gesture is determined to be a |
| * swipe or a pinch, further 2-finger motion-events will be ignored. When a different event is |
| * passed in (motion event with != 2 pointers, or some other event type), this object will |
| * revert back to the original UNKNOWN state. |
| */ |
| public void onTouchEvent(MotionEvent event) { |
| if (event.getPointerCount() != 2) { |
| reset(); |
| return; |
| } |
| |
| // Only MOVE or DOWN events are considered - all other events should finish any current |
| // gesture and reset the detector. In addition, a DOWN event should reset the detector, |
| // since it signals the start of the gesture. If the events are consistent, a DOWN event |
| // will occur at the start of the gesture, but this implementation tries to cope in case |
| // the first event is MOVE rather than DOWN. |
| int action = event.getActionMasked(); |
| if (action != MotionEvent.ACTION_MOVE) { |
| reset(); |
| if (action != MotionEvent.ACTION_POINTER_DOWN) { |
| return; |
| } |
| } |
| |
| // If the gesture is known, there is no need for further processing - the state should |
| // remain the same until the gesture is complete, as tested above. |
| if (mState != State.UNKNOWN) { |
| return; |
| } |
| |
| float currentX0 = event.getX(0); |
| float currentY0 = event.getY(0); |
| float currentX1 = event.getX(1); |
| float currentY1 = event.getY(1); |
| if (!mInGesture) { |
| // This is the first event of the gesture, so store the pointer coordinates. |
| mFirstX0 = currentX0; |
| mFirstY0 = currentY0; |
| mFirstX1 = currentX1; |
| mFirstY1 = currentY1; |
| mInGesture = true; |
| return; |
| } |
| |
| float deltaX0 = currentX0 - mFirstX0; |
| float deltaY0 = currentY0 - mFirstY0; |
| float deltaX1 = currentX1 - mFirstX1; |
| float deltaY1 = currentY1 - mFirstY1; |
| |
| float squaredDistance0 = deltaX0 * deltaX0 + deltaY0 * deltaY0; |
| float squaredDistance1 = deltaX1 * deltaX1 + deltaY1 * deltaY1; |
| |
| |
| // If both fingers have moved beyond the touch-slop, it is safe to recognize the gesture. |
| // However, one finger might be held stationary whilst the other finger is moved a long |
| // distance. In this case, it is preferable to trigger a PINCH. This should be detected |
| // soon enough to avoid triggering a sudden large change in the zoom level, but not so |
| // soon that SWIPE never gets triggered. |
| |
| // Threshold level for triggering the PINCH gesture if one finger is stationary. This |
| // cannot be equal to the touch-slop, because in that case, SWIPE would rarely be detected. |
| // One finger would usually leave the touch-slop radius slightly before the other finger, |
| // triggering a PINCH as described above. A larger radius gives an opportunity for |
| // SWIPE to be detected. Doubling the radius is an arbitrary choice that works well. |
| int pinchThresholdSquare = 4 * mTouchSlopSquare; |
| |
| boolean finger0Moved = squaredDistance0 > mTouchSlopSquare; |
| boolean finger1Moved = squaredDistance1 > mTouchSlopSquare; |
| |
| if (!finger0Moved && !finger1Moved) { |
| return; |
| } |
| |
| if (finger0Moved && !finger1Moved) { |
| if (squaredDistance0 > pinchThresholdSquare) { |
| mState = State.PINCH; |
| } |
| return; |
| } |
| |
| if (!finger0Moved && finger1Moved) { |
| if (squaredDistance1 > pinchThresholdSquare) { |
| mState = State.PINCH; |
| } |
| return; |
| } |
| |
| // Both fingers have moved, so determine SWIPE/PINCH status. If the fingers have moved in |
| // the same direction, this is a SWIPE, otherwise it's a PINCH. This can be measured by |
| // taking the scalar product of the direction vectors. This product is positive if the |
| // vectors are pointing in the same direction, and negative if they're in opposite |
| // directions. |
| float scalarProduct = deltaX0 * deltaX1 + deltaY0 * deltaY1; |
| mState = (scalarProduct > 0) ? State.SWIPE : State.PINCH; |
| } |
| } |