1 // Copyright (c) 2012 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.content.browser;
7 import android.content.Context;
8 import android.os.Handler;
9 import android.os.Message;
10 import android.view.MotionEvent;
11 import android.view.ViewConfiguration;
13 import java.util.Iterator;
16 * This class controls long press timers and is owned by a ContentViewGestureHandler.
18 * For instance, we may receive a DOWN then UP, so we may need to cancel the
19 * timer before the UP completes its roundtrip from WebKit.
21 class LongPressDetector {
22 private MotionEvent mCurrentDownEvent;
23 private final LongPressDelegate mLongPressDelegate;
24 private final Handler mLongPressHandler;
25 private final int mTouchSlopSquare;
26 private boolean mInLongPress;
28 // The following are used when touch events are offered to native, and not for
29 // anything relating to the GestureDetector.
30 // True iff a touch_move has exceeded the touch slop distance.
31 private boolean mMoveConfirmed;
32 // Coordinates of the start of a touch event (i.e. the touch_down).
33 private int mTouchInitialX;
34 private int mTouchInitialY;
36 private static final int LONG_PRESS = 2;
38 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
39 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
41 LongPressDetector(Context context, LongPressDelegate delegate) {
42 mLongPressDelegate = delegate;
43 mLongPressHandler = new LongPressHandler();
44 final ViewConfiguration configuration = ViewConfiguration.get(context);
45 int touchSlop = configuration.getScaledTouchSlop();
46 mTouchSlopSquare = touchSlop * touchSlop;
49 private class LongPressHandler extends Handler {
55 public void handleMessage(Message msg) {
61 throw new RuntimeException("Unknown message " + msg); //never
67 * This is an interface to execute the LongPress when it receives the onLongPress message.
69 interface LongPressDelegate {
71 * @param event The event will be recycled after this call has returned.
73 public void onLongPress(MotionEvent event);
76 private static long calculateLongPressTimeoutTime(MotionEvent ev) {
77 // Using getEventTime instead of getDownTime since for Android WebView,
78 // event stream can be arbitrarily delayed.
79 return ev.getEventTime() + TAP_TIMEOUT + LONGPRESS_TIMEOUT;
83 * Initiates a LONG_PRESS gesture timer if needed.
85 void startLongPressTimerIfNeeded(MotionEvent ev) {
86 if (ev.getAction() != MotionEvent.ACTION_DOWN) return;
88 // If there is a current down, we do not expect another down event before
89 // receiving an up event
90 if (mCurrentDownEvent != null) return;
92 mCurrentDownEvent = MotionEvent.obtain(ev);
93 mLongPressHandler.sendEmptyMessageAtTime(LONG_PRESS,
94 calculateLongPressTimeoutTime(mCurrentDownEvent));
98 // Cancel LONG_PRESS timers.
99 void cancelLongPressIfNeeded(MotionEvent ev) {
100 if (!hasPendingMessage() ||
101 mCurrentDownEvent == null || ev.getDownTime() != mCurrentDownEvent.getDownTime()) {
104 final int action = ev.getAction();
105 final float y = ev.getY();
106 final float x = ev.getX();
107 switch (action & MotionEvent.ACTION_MASK) {
108 case MotionEvent.ACTION_MOVE:
109 final int deltaX = (int) (x - mCurrentDownEvent.getX());
110 final int deltaY = (int) (y - mCurrentDownEvent.getY());
111 int distance = (deltaX * deltaX) + (deltaY * deltaY);
112 if (distance > mTouchSlopSquare) {
116 case MotionEvent.ACTION_UP:
117 case MotionEvent.ACTION_CANCEL:
118 if (calculateLongPressTimeoutTime(mCurrentDownEvent) > ev.getEventTime()) {
127 // Given a stream of pending events, cancel the LONG_PRESS timer if appropriate.
128 void cancelLongPressIfNeeded(Iterator<MotionEvent> pendingEvents) {
129 if (mCurrentDownEvent == null)
131 long currentDownTime = mCurrentDownEvent.getDownTime();
132 while (pendingEvents.hasNext()) {
133 MotionEvent pending = pendingEvents.next();
134 if (pending.getDownTime() != currentDownTime) {
137 cancelLongPressIfNeeded(pending);
141 void cancelLongPress() {
142 mInLongPress = false;
143 if (hasPendingMessage()) {
144 mLongPressHandler.removeMessages(LONG_PRESS);
145 mCurrentDownEvent.recycle();
146 mCurrentDownEvent = null;
150 // Used this to check if a onSingleTapUp is part of a long press event.
151 boolean isInLongPress() {
155 private void dispatchLongPress() {
157 mLongPressDelegate.onLongPress(mCurrentDownEvent);
158 mCurrentDownEvent.recycle();
159 mCurrentDownEvent = null;
162 boolean hasPendingMessage() {
163 return mCurrentDownEvent != null;
166 void onOfferTouchEventToJavaScript(MotionEvent event) {
167 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
168 mMoveConfirmed = false;
169 mTouchInitialX = Math.round(event.getX());
170 mTouchInitialY = Math.round(event.getY());
174 boolean confirmOfferMoveEventToJavaScript(MotionEvent event) {
175 if (!mMoveConfirmed) {
176 int deltaX = Math.round(event.getX()) - mTouchInitialX;
177 int deltaY = Math.round(event.getY()) - mTouchInitialY;
178 if (deltaX * deltaX + deltaY * deltaY >= mTouchSlopSquare) {
179 mMoveConfirmed = true;
182 return mMoveConfirmed;
186 * This is for testing only.
187 * Sends a LongPress gesture. This should always be called after a down event.
189 void sendLongPressGestureForTest() {
190 if (mCurrentDownEvent == null) return;