• 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.graphics.PointF;
9 import android.os.Handler;
10 import android.os.Message;
11 import android.util.SparseArray;
12 import android.view.MotionEvent;
13 import android.view.ViewConfiguration;
14 
15 /**
16  * This class detects multi-finger tap and long-press events. This is provided since the stock
17  * Android gesture-detectors only detect taps/long-presses made with one finger.
18  */
19 public class TapGestureDetector {
20     /** The listener for receiving notifications of tap gestures. */
21     public interface OnTapListener {
22         /**
23          * Notified when a tap event occurs.
24          *
25          * @param pointerCount The number of fingers that were tapped.
26          * @return True if the event is consumed.
27          */
onTap(int pointerCount)28         boolean onTap(int pointerCount);
29 
30         /**
31          * Notified when a long-touch event occurs.
32          *
33          * @param pointerCount The number of fingers held down.
34          */
onLongPress(int pointerCount)35         void onLongPress(int pointerCount);
36     }
37 
38     /** The listener to which notifications are sent. */
39     private OnTapListener mListener;
40 
41     /** Handler used for posting tasks to be executed in the future. */
42     private Handler mHandler;
43 
44     /** The maximum number of fingers seen in the gesture. */
45     private int mPointerCount = 0;
46 
47     /**
48      * Stores the location of each down MotionEvent (by pointer ID), for detecting motion of any
49      * pointer beyond the TouchSlop region.
50      */
51     private SparseArray<PointF> mInitialPositions = new SparseArray<PointF>();
52 
53     /**
54      * Threshold squared-distance, in pixels, to use for motion-detection. If a finger moves less
55      * than this distance, the gesture is still eligible to be a tap event.
56      */
57     private int mTouchSlopSquare;
58 
59     /** Set to true whenever motion is detected in the gesture, or a long-touch is triggered. */
60     private boolean mTapCancelled = false;
61 
62     private class EventHandler extends Handler {
63         @Override
handleMessage(Message message)64         public void handleMessage(Message message) {
65             mListener.onLongPress(mPointerCount);
66             mTapCancelled = true;
67         }
68     }
69 
TapGestureDetector(Context context, OnTapListener listener)70     public TapGestureDetector(Context context, OnTapListener listener) {
71         mListener = listener;
72         mHandler = new EventHandler();
73         ViewConfiguration config = ViewConfiguration.get(context);
74         int touchSlop = config.getScaledTouchSlop();
75         mTouchSlopSquare = touchSlop * touchSlop;
76     }
77 
78     /** Analyzes the touch event to determine whether to notify the listener. */
onTouchEvent(MotionEvent event)79     public boolean onTouchEvent(MotionEvent event) {
80         boolean handled = false;
81         switch (event.getActionMasked()) {
82             case MotionEvent.ACTION_DOWN:
83                 reset();
84                 // Cause a long-press notification to be triggered after the timeout.
85                 mHandler.sendEmptyMessageDelayed(0, ViewConfiguration.getLongPressTimeout());
86                 trackDownEvent(event);
87                 mPointerCount = 1;
88                 break;
89 
90             case MotionEvent.ACTION_POINTER_DOWN:
91                 trackDownEvent(event);
92                 mPointerCount = Math.max(mPointerCount, event.getPointerCount());
93                 break;
94 
95             case MotionEvent.ACTION_MOVE:
96                 if (!mTapCancelled) {
97                     if (trackMoveEvent(event)) {
98                         cancelLongTouchNotification();
99                         mTapCancelled = true;
100                     }
101                 }
102                 break;
103 
104             case MotionEvent.ACTION_UP:
105                 cancelLongTouchNotification();
106                 if (!mTapCancelled) {
107                     handled = mListener.onTap(mPointerCount);
108                 }
109                 break;
110 
111             case MotionEvent.ACTION_POINTER_UP:
112                 cancelLongTouchNotification();
113                 trackUpEvent(event);
114                 break;
115 
116             case MotionEvent.ACTION_CANCEL:
117                 cancelLongTouchNotification();
118                 break;
119 
120             default:
121                 break;
122         }
123         return handled;
124     }
125 
126     /** Stores the location of the ACTION_DOWN or ACTION_POINTER_DOWN event. */
trackDownEvent(MotionEvent event)127     private void trackDownEvent(MotionEvent event) {
128         int pointerIndex = 0;
129         if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
130             pointerIndex = event.getActionIndex();
131         }
132         int pointerId = event.getPointerId(pointerIndex);
133         mInitialPositions.put(pointerId,
134                 new PointF(event.getX(pointerIndex), event.getY(pointerIndex)));
135     }
136 
137     /** Removes the ACTION_UP or ACTION_POINTER_UP event from the stored list. */
trackUpEvent(MotionEvent event)138     private void trackUpEvent(MotionEvent event) {
139         int pointerIndex = 0;
140         if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
141             pointerIndex = event.getActionIndex();
142         }
143         int pointerId = event.getPointerId(pointerIndex);
144         mInitialPositions.remove(pointerId);
145     }
146 
147     /**
148      * Processes an ACTION_MOVE event and returns whether a pointer moved beyond the TouchSlop
149      * threshold.
150      *
151      * @return True if motion was detected.
152      */
trackMoveEvent(MotionEvent event)153     private boolean trackMoveEvent(MotionEvent event) {
154         int pointerCount = event.getPointerCount();
155         for (int i = 0; i < pointerCount; i++) {
156             int pointerId = event.getPointerId(i);
157             float currentX = event.getX(i);
158             float currentY = event.getY(i);
159             PointF downPoint = mInitialPositions.get(pointerId);
160             if (downPoint == null) {
161                 // There was no corresponding DOWN event, so add it. This is an inconsistency
162                 // which shouldn't normally occur.
163                 mInitialPositions.put(pointerId, new PointF(currentX, currentY));
164                 continue;
165             }
166             float deltaX = currentX - downPoint.x;
167             float deltaY = currentY - downPoint.y;
168             if (deltaX * deltaX + deltaY * deltaY > mTouchSlopSquare) {
169                 return true;
170             }
171         }
172         return false;
173     }
174 
175     /** Cleans up any stored data for the gesture. */
reset()176     private void reset() {
177         cancelLongTouchNotification();
178         mPointerCount = 0;
179         mInitialPositions.clear();
180         mTapCancelled = false;
181     }
182 
183     /** Cancels any pending long-touch notifications from the message-queue. */
cancelLongTouchNotification()184     private void cancelLongTouchNotification() {
185         mHandler.removeMessages(0);
186     }
187 }
188