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
5package org.chromium.chromoting;
6
7import android.content.Context;
8import android.view.MotionEvent;
9import 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 */
16public 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