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 */ 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 */ 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 64 public void handleMessage(Message message) { 65 mListener.onLongPress(mPointerCount); 66 mTapCancelled = true; 67 } 68 } 69 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. */ 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. */ 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. */ 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 */ 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. */ 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. */ 184 private void cancelLongTouchNotification() { 185 mHandler.removeMessages(0); 186 } 187 } 188