blob: 23f3e3c3ece9df010bcec53a4d2897ac957baf6f [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view;
18
19import android.os.Handler;
20import android.os.Message;
21import android.content.Context;
22
23/**
24 * Detects various gestures and events using the supplied {@link MotionEvent}s.
25 * The {@link OnGestureListener} callback will notify users when a particular
26 * motion event has occurred. This class should only be used with {@link MotionEvent}s
27 * reported via touch (don't use for trackball events).
28 *
29 * To use this class:
30 * <ul>
31 * <li>Create an instance of the {@code GestureDetector} for your {@link View}
32 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
33 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
34 * will be executed when the events occur.
35 * </ul>
36 */
37public class GestureDetector {
38 /**
39 * The listener that is used to notify when gestures occur.
40 * If you want to listen for all the different gestures then implement
41 * this interface. If you only want to listen for a subset it might
42 * be easier to extend {@link SimpleOnGestureListener}.
43 */
44 public interface OnGestureListener {
45
46 /**
47 * Notified when a tap occurs with the down {@link MotionEvent}
48 * that triggered it. This will be triggered immediately for
49 * every down event. All other events should be preceded by this.
50 *
51 * @param e The down motion event.
52 */
53 boolean onDown(MotionEvent e);
54
55 /**
56 * The user has performed a down {@link MotionEvent} and not performed
57 * a move or up yet. This event is commonly used to provide visual
58 * feedback to the user to let them know that their action has been
59 * recognized i.e. highlight an element.
60 *
61 * @param e The down motion event
62 */
63 void onShowPress(MotionEvent e);
64
65 /**
66 * Notified when a tap occurs with the up {@link MotionEvent}
67 * that triggered it.
68 *
69 * @param e The up motion event that completed the first tap
70 * @return true if the event is consumed, else false
71 */
72 boolean onSingleTapUp(MotionEvent e);
73
74 /**
75 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
76 * current move {@link MotionEvent}. The distance in x and y is also supplied for
77 * convenience.
78 *
79 * @param e1 The first down motion event that started the scrolling.
80 * @param e2 The move motion event that triggered the current onScroll.
81 * @param distanceX The distance along the X axis that has been scrolled since the last
82 * call to onScroll. This is NOT the distance between {@code e1}
83 * and {@code e2}.
84 * @param distanceY The distance along the Y axis that has been scrolled since the last
85 * call to onScroll. This is NOT the distance between {@code e1}
86 * and {@code e2}.
87 * @return true if the event is consumed, else false
88 */
89 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
90
91 /**
92 * Notified when a long press occurs with the initial on down {@link MotionEvent}
93 * that trigged it.
94 *
95 * @param e The initial on down motion event that started the longpress.
96 */
97 void onLongPress(MotionEvent e);
98
99 /**
100 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
101 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
102 * the x and y axis in pixels per second.
103 *
104 * @param e1 The first down motion event that started the fling.
105 * @param e2 The move motion event that triggered the current onFling.
106 * @param velocityX The velocity of this fling measured in pixels per second
107 * along the x axis.
108 * @param velocityY The velocity of this fling measured in pixels per second
109 * along the y axis.
110 * @return true if the event is consumed, else false
111 */
112 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
113 }
114
115 /**
116 * The listener that is used to notify when a double-tap or a confirmed
117 * single-tap occur.
118 */
119 public interface OnDoubleTapListener {
120 /**
121 * Notified when a single-tap occurs.
122 * <p>
123 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
124 * will only be called after the detector is confident that the user's
125 * first tap is not followed by a second tap leading to a double-tap
126 * gesture.
127 *
128 * @param e The down motion event of the single-tap.
129 * @return true if the event is consumed, else false
130 */
131 boolean onSingleTapConfirmed(MotionEvent e);
132
133 /**
134 * Notified when a double-tap occurs.
135 *
136 * @param e The down motion event of the first tap of the double-tap.
137 * @return true if the event is consumed, else false
138 */
139 boolean onDoubleTap(MotionEvent e);
140
141 /**
142 * Notified when an event within a double-tap gesture occurs, including
143 * the down, move, and up events.
144 *
145 * @param e The motion event that occurred during the double-tap gesture.
146 * @return true if the event is consumed, else false
147 */
148 boolean onDoubleTapEvent(MotionEvent e);
149 }
150
151 /**
152 * A convenience class to extend when you only want to listen for a subset
153 * of all the gestures. This implements all methods in the
154 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
155 * nothing and return {@code false} for all applicable methods.
156 */
157 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
158 public boolean onSingleTapUp(MotionEvent e) {
159 return false;
160 }
161
162 public void onLongPress(MotionEvent e) {
163 }
164
165 public boolean onScroll(MotionEvent e1, MotionEvent e2,
166 float distanceX, float distanceY) {
167 return false;
168 }
169
170 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
171 float velocityY) {
172 return false;
173 }
174
175 public void onShowPress(MotionEvent e) {
176 }
177
178 public boolean onDown(MotionEvent e) {
179 return false;
180 }
181
182 public boolean onDoubleTap(MotionEvent e) {
183 return false;
184 }
185
186 public boolean onDoubleTapEvent(MotionEvent e) {
187 return false;
188 }
189
190 public boolean onSingleTapConfirmed(MotionEvent e) {
191 return false;
192 }
193 }
194
195 // TODO: ViewConfiguration
196 private int mBiggerTouchSlopSquare = 20 * 20;
197
198 private int mTouchSlopSquare;
199 private int mDoubleTapSlopSquare;
200 private int mMinimumFlingVelocity;
201
202 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
203 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800204 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
205
206 // constants for Message.what used by GestureHandler below
207 private static final int SHOW_PRESS = 1;
208 private static final int LONG_PRESS = 2;
209 private static final int TAP = 3;
210
211 private final Handler mHandler;
212 private final OnGestureListener mListener;
213 private OnDoubleTapListener mDoubleTapListener;
214
215 private boolean mStillDown;
216 private boolean mInLongPress;
217 private boolean mAlwaysInTapRegion;
218 private boolean mAlwaysInBiggerTapRegion;
219
220 private MotionEvent mCurrentDownEvent;
221 private MotionEvent mPreviousUpEvent;
222
223 /**
224 * True when the user is still touching for the second tap (down, move, and
225 * up events). Can only be true if there is a double tap listener attached.
226 */
227 private boolean mIsDoubleTapping;
228
229 private float mLastMotionY;
230 private float mLastMotionX;
231
232 private boolean mIsLongpressEnabled;
233
234 /**
235 * Determines speed during touch scrolling
236 */
237 private VelocityTracker mVelocityTracker;
238
239 private class GestureHandler extends Handler {
240 GestureHandler() {
241 super();
242 }
243
244 GestureHandler(Handler handler) {
245 super(handler.getLooper());
246 }
247
248 @Override
249 public void handleMessage(Message msg) {
250 switch (msg.what) {
251 case SHOW_PRESS:
252 mListener.onShowPress(mCurrentDownEvent);
253 break;
254
255 case LONG_PRESS:
256 dispatchLongPress();
257 break;
258
259 case TAP:
260 // If the user's finger is still down, do not count it as a tap
261 if (mDoubleTapListener != null && !mStillDown) {
262 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
263 }
264 break;
265
266 default:
267 throw new RuntimeException("Unknown message " + msg); //never
268 }
269 }
270 }
271
272 /**
273 * Creates a GestureDetector with the supplied listener.
274 * This variant of the constructor should be used from a non-UI thread
275 * (as it allows specifying the Handler).
276 *
277 * @param listener the listener invoked for all the callbacks, this must
278 * not be null.
279 * @param handler the handler to use
280 *
281 * @throws NullPointerException if either {@code listener} or
282 * {@code handler} is null.
283 *
284 * @deprecated Use {@link #GestureDetector(android.content.Context,
285 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
286 */
287 @Deprecated
288 public GestureDetector(OnGestureListener listener, Handler handler) {
289 this(null, listener, handler);
290 }
291
292 /**
293 * Creates a GestureDetector with the supplied listener.
294 * You may only use this constructor from a UI thread (this is the usual situation).
295 * @see android.os.Handler#Handler()
296 *
297 * @param listener the listener invoked for all the callbacks, this must
298 * not be null.
299 *
300 * @throws NullPointerException if {@code listener} is null.
301 *
302 * @deprecated Use {@link #GestureDetector(android.content.Context,
303 * android.view.GestureDetector.OnGestureListener)} instead.
304 */
305 @Deprecated
306 public GestureDetector(OnGestureListener listener) {
307 this(null, listener, null);
308 }
309
310 /**
311 * Creates a GestureDetector with the supplied listener.
312 * You may only use this constructor from a UI thread (this is the usual situation).
313 * @see android.os.Handler#Handler()
314 *
315 * @param context the application's context
316 * @param listener the listener invoked for all the callbacks, this must
317 * not be null.
318 *
319 * @throws NullPointerException if {@code listener} is null.
320 */
321 public GestureDetector(Context context, OnGestureListener listener) {
322 this(context, listener, null);
323 }
324
325 /**
326 * Creates a GestureDetector with the supplied listener.
327 * You may only use this constructor from a UI thread (this is the usual situation).
328 * @see android.os.Handler#Handler()
329 *
330 * @param context the application's context
331 * @param listener the listener invoked for all the callbacks, this must
332 * not be null.
333 * @param handler the handler to use
334 *
335 * @throws NullPointerException if {@code listener} is null.
336 */
337 public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
338 if (handler != null) {
339 mHandler = new GestureHandler(handler);
340 } else {
341 mHandler = new GestureHandler();
342 }
343 mListener = listener;
344 if (listener instanceof OnDoubleTapListener) {
345 setOnDoubleTapListener((OnDoubleTapListener) listener);
346 }
347 init(context);
348 }
349
350 private void init(Context context) {
351 if (mListener == null) {
352 throw new NullPointerException("OnGestureListener must not be null");
353 }
354 mIsLongpressEnabled = true;
355
356 // Fallback to support pre-donuts releases
357 int touchSlop, doubleTapSlop;
358 if (context == null) {
359 //noinspection deprecation
360 touchSlop = ViewConfiguration.getTouchSlop();
361 doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
362 //noinspection deprecation
363 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
364 } else {
365 final ViewConfiguration configuration = ViewConfiguration.get(context);
366 touchSlop = configuration.getScaledTouchSlop();
367 doubleTapSlop = configuration.getScaledDoubleTapSlop();
368 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
369 }
370 mTouchSlopSquare = touchSlop * touchSlop;
371 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
372 }
373
374 /**
375 * Sets the listener which will be called for double-tap and related
376 * gestures.
377 *
378 * @param onDoubleTapListener the listener invoked for all the callbacks, or
379 * null to stop listening for double-tap gestures.
380 */
381 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
382 mDoubleTapListener = onDoubleTapListener;
383 }
384
385 /**
386 * Set whether longpress is enabled, if this is enabled when a user
387 * presses and holds down you get a longpress event and nothing further.
388 * If it's disabled the user can press and hold down and then later
389 * moved their finger and you will get scroll events. By default
390 * longpress is enabled.
391 *
392 * @param isLongpressEnabled whether longpress should be enabled.
393 */
394 public void setIsLongpressEnabled(boolean isLongpressEnabled) {
395 mIsLongpressEnabled = isLongpressEnabled;
396 }
397
398 /**
399 * @return true if longpress is enabled, else false.
400 */
401 public boolean isLongpressEnabled() {
402 return mIsLongpressEnabled;
403 }
404
405 /**
406 * Analyzes the given motion event and if applicable triggers the
407 * appropriate callbacks on the {@link OnGestureListener} supplied.
408 *
409 * @param ev The current motion event.
410 * @return true if the {@link OnGestureListener} consumed the event,
411 * else false.
412 */
413 public boolean onTouchEvent(MotionEvent ev) {
414 final int action = ev.getAction();
415 final float y = ev.getY();
416 final float x = ev.getX();
417
418 if (mVelocityTracker == null) {
419 mVelocityTracker = VelocityTracker.obtain();
420 }
421 mVelocityTracker.addMovement(ev);
422
423 boolean handled = false;
424
425 switch (action) {
426 case MotionEvent.ACTION_DOWN:
427 if (mDoubleTapListener != null) {
428 boolean hadTapMessage = mHandler.hasMessages(TAP);
429 if (hadTapMessage) mHandler.removeMessages(TAP);
430 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
431 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
432 // This is a second tap
433 mIsDoubleTapping = true;
434 // Give a callback with the first tap of the double-tap
435 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
436 // Give a callback with down event of the double-tap
437 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
438 } else {
439 // This is a first tap
440 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
441 }
442 }
443
444 mLastMotionX = x;
445 mLastMotionY = y;
446 mCurrentDownEvent = MotionEvent.obtain(ev);
447 mAlwaysInTapRegion = true;
448 mAlwaysInBiggerTapRegion = true;
449 mStillDown = true;
450 mInLongPress = false;
451
452 if (mIsLongpressEnabled) {
453 mHandler.removeMessages(LONG_PRESS);
454 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
455 + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
456 }
457 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
458 handled |= mListener.onDown(ev);
459 break;
460
461 case MotionEvent.ACTION_MOVE:
462 if (mInLongPress) {
463 break;
464 }
465 final float scrollX = mLastMotionX - x;
466 final float scrollY = mLastMotionY - y;
467 if (mIsDoubleTapping) {
468 // Give the move events of the double-tap
469 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
470 } else if (mAlwaysInTapRegion) {
471 final int deltaX = (int) (x - mCurrentDownEvent.getX());
472 final int deltaY = (int) (y - mCurrentDownEvent.getY());
473 int distance = (deltaX * deltaX) + (deltaY * deltaY);
474 if (distance > mTouchSlopSquare) {
475 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
476 mLastMotionX = x;
477 mLastMotionY = y;
478 mAlwaysInTapRegion = false;
479 mHandler.removeMessages(TAP);
480 mHandler.removeMessages(SHOW_PRESS);
481 mHandler.removeMessages(LONG_PRESS);
482 }
483 if (distance > mBiggerTouchSlopSquare) {
484 mAlwaysInBiggerTapRegion = false;
485 }
486 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
487 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
488 mLastMotionX = x;
489 mLastMotionY = y;
490 }
491 break;
492
493 case MotionEvent.ACTION_UP:
494 mStillDown = false;
495 MotionEvent currentUpEvent = MotionEvent.obtain(ev);
496 if (mIsDoubleTapping) {
497 // Finally, give the up event of the double-tap
498 handled |= mDoubleTapListener.onDoubleTapEvent(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800499 } else if (mInLongPress) {
500 mHandler.removeMessages(TAP);
501 mInLongPress = false;
The Android Open Source Projectf5b4b982009-03-05 20:00:43 -0800502 } else if (mAlwaysInTapRegion) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503 handled = mListener.onSingleTapUp(ev);
504 } else {
505
506 // A fling must travel the minimum tap distance
507 final VelocityTracker velocityTracker = mVelocityTracker;
508 velocityTracker.computeCurrentVelocity(1000);
509 final float velocityY = velocityTracker.getYVelocity();
510 final float velocityX = velocityTracker.getXVelocity();
511
512 if ((Math.abs(velocityY) > mMinimumFlingVelocity)
513 || (Math.abs(velocityX) > mMinimumFlingVelocity)){
514 handled = mListener.onFling(mCurrentDownEvent, currentUpEvent, velocityX, velocityY);
515 }
516 }
517 mPreviousUpEvent = MotionEvent.obtain(ev);
518 mVelocityTracker.recycle();
519 mVelocityTracker = null;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700520 mIsDoubleTapping = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800521 mHandler.removeMessages(SHOW_PRESS);
522 mHandler.removeMessages(LONG_PRESS);
523 break;
524 case MotionEvent.ACTION_CANCEL:
525 mHandler.removeMessages(SHOW_PRESS);
526 mHandler.removeMessages(LONG_PRESS);
527 mHandler.removeMessages(TAP);
528 mVelocityTracker.recycle();
529 mVelocityTracker = null;
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700530 mIsDoubleTapping = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800531 mStillDown = false;
532 if (mInLongPress) {
533 mInLongPress = false;
534 break;
535 }
536 }
537 return handled;
538 }
539
540 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
541 MotionEvent secondDown) {
542 if (!mAlwaysInBiggerTapRegion) {
543 return false;
544 }
545
546 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
547 return false;
548 }
549
550 int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
551 int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
552 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
553 }
554
555 private void dispatchLongPress() {
556 mHandler.removeMessages(TAP);
557 mInLongPress = true;
558 mListener.onLongPress(mCurrentDownEvent);
559 }
560}