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.
5 package org
.chromium
.chromoting
;
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
;
15 import java
.lang
.ref
.WeakReference
;
18 * This class detects multi-finger tap and long-press events. This is provided since the stock
19 * Android gesture-detectors only detect taps/long-presses made with one finger.
21 public class TapGestureDetector
{
22 /** The listener for receiving notifications of tap gestures. */
23 public interface OnTapListener
{
25 * Notified when a tap event occurs.
27 * @param pointerCount The number of fingers that were tapped.
28 * @return True if the event is consumed.
30 boolean onTap(int pointerCount
);
33 * Notified when a long-touch event occurs.
35 * @param pointerCount The number of fingers held down.
37 void onLongPress(int pointerCount
);
40 /** The listener to which notifications are sent. */
41 private OnTapListener mListener
;
43 /** Handler used for posting tasks to be executed in the future. */
44 private Handler mHandler
;
46 /** The maximum number of fingers seen in the gesture. */
47 private int mPointerCount
= 0;
50 * Stores the location of each down MotionEvent (by pointer ID), for detecting motion of any
51 * pointer beyond the TouchSlop region.
53 private SparseArray
<PointF
> mInitialPositions
= new SparseArray
<PointF
>();
56 * Threshold squared-distance, in pixels, to use for motion-detection. If a finger moves less
57 * than this distance, the gesture is still eligible to be a tap event.
59 private int mTouchSlopSquare
;
61 /** Set to true whenever motion is detected in the gesture, or a long-touch is triggered. */
62 private boolean mTapCancelled
= false;
64 // This static inner class holds a WeakReference to the outer object, to avoid triggering the
65 // lint HandlerLeak warning.
66 private static class EventHandler
extends Handler
{
67 private final WeakReference
<TapGestureDetector
> mDetector
;
69 public EventHandler(TapGestureDetector detector
) {
70 mDetector
= new WeakReference
<TapGestureDetector
>(detector
);
74 public void handleMessage(Message message
) {
75 TapGestureDetector detector
= mDetector
.get();
76 if (detector
!= null) {
77 detector
.mListener
.onLongPress(detector
.mPointerCount
);
78 detector
.mTapCancelled
= true;
83 public TapGestureDetector(Context context
, OnTapListener listener
) {
85 mHandler
= new EventHandler(this);
86 ViewConfiguration config
= ViewConfiguration
.get(context
);
87 int touchSlop
= config
.getScaledTouchSlop();
88 mTouchSlopSquare
= touchSlop
* touchSlop
;
91 /** Analyzes the touch event to determine whether to notify the listener. */
92 public boolean onTouchEvent(MotionEvent event
) {
93 boolean handled
= false;
94 switch (event
.getActionMasked()) {
95 case MotionEvent
.ACTION_DOWN
:
97 // Cause a long-press notification to be triggered after the timeout.
98 mHandler
.sendEmptyMessageDelayed(0, ViewConfiguration
.getLongPressTimeout());
99 trackDownEvent(event
);
103 case MotionEvent
.ACTION_POINTER_DOWN
:
104 trackDownEvent(event
);
105 mPointerCount
= Math
.max(mPointerCount
, event
.getPointerCount());
108 case MotionEvent
.ACTION_MOVE
:
109 if (!mTapCancelled
) {
110 if (trackMoveEvent(event
)) {
111 cancelLongTouchNotification();
112 mTapCancelled
= true;
117 case MotionEvent
.ACTION_UP
:
118 cancelLongTouchNotification();
119 if (!mTapCancelled
) {
120 handled
= mListener
.onTap(mPointerCount
);
124 case MotionEvent
.ACTION_POINTER_UP
:
125 cancelLongTouchNotification();
129 case MotionEvent
.ACTION_CANCEL
:
130 cancelLongTouchNotification();
139 /** Stores the location of the ACTION_DOWN or ACTION_POINTER_DOWN event. */
140 private void trackDownEvent(MotionEvent event
) {
141 int pointerIndex
= 0;
142 if (event
.getActionMasked() == MotionEvent
.ACTION_POINTER_DOWN
) {
143 pointerIndex
= event
.getActionIndex();
145 int pointerId
= event
.getPointerId(pointerIndex
);
146 mInitialPositions
.put(pointerId
,
147 new PointF(event
.getX(pointerIndex
), event
.getY(pointerIndex
)));
150 /** Removes the ACTION_UP or ACTION_POINTER_UP event from the stored list. */
151 private void trackUpEvent(MotionEvent event
) {
152 int pointerIndex
= 0;
153 if (event
.getActionMasked() == MotionEvent
.ACTION_POINTER_UP
) {
154 pointerIndex
= event
.getActionIndex();
156 int pointerId
= event
.getPointerId(pointerIndex
);
157 mInitialPositions
.remove(pointerId
);
161 * Processes an ACTION_MOVE event and returns whether a pointer moved beyond the TouchSlop
164 * @return True if motion was detected.
166 private boolean trackMoveEvent(MotionEvent event
) {
167 int pointerCount
= event
.getPointerCount();
168 for (int i
= 0; i
< pointerCount
; i
++) {
169 int pointerId
= event
.getPointerId(i
);
170 float currentX
= event
.getX(i
);
171 float currentY
= event
.getY(i
);
172 PointF downPoint
= mInitialPositions
.get(pointerId
);
173 if (downPoint
== null) {
174 // There was no corresponding DOWN event, so add it. This is an inconsistency
175 // which shouldn't normally occur.
176 mInitialPositions
.put(pointerId
, new PointF(currentX
, currentY
));
179 float deltaX
= currentX
- downPoint
.x
;
180 float deltaY
= currentY
- downPoint
.y
;
181 if (deltaX
* deltaX
+ deltaY
* deltaY
> mTouchSlopSquare
) {
188 /** Cleans up any stored data for the gesture. */
189 private void reset() {
190 cancelLongTouchNotification();
192 mInitialPositions
.clear();
193 mTapCancelled
= false;
196 /** Cancels any pending long-touch notifications from the message-queue. */
197 private void cancelLongTouchNotification() {
198 mHandler
.removeMessages(0);