blob: 8f3b17f517a7bb1b5298f366de76c4bf45f56e10 [file] [log] [blame]
// 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;
}
}