GestureDetector.java revision f90165aeda1a8353c1b5e837b1ef4a818ecbefc5
1/* 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.content.Context; 20import android.os.Build; 21import android.os.Handler; 22import android.os.Message; 23 24/** 25 * Detects various gestures and events using the supplied {@link MotionEvent}s. 26 * The {@link OnGestureListener} callback will notify users when a particular 27 * motion event has occurred. This class should only be used with {@link MotionEvent}s 28 * reported via touch (don't use for trackball events). 29 * 30 * To use this class: 31 * <ul> 32 * <li>Create an instance of the {@code GestureDetector} for your {@link View} 33 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 34 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback 35 * will be executed when the events occur. 36 * </ul> 37 */ 38public class GestureDetector { 39 /** 40 * The listener that is used to notify when gestures occur. 41 * If you want to listen for all the different gestures then implement 42 * this interface. If you only want to listen for a subset it might 43 * be easier to extend {@link SimpleOnGestureListener}. 44 */ 45 public interface OnGestureListener { 46 47 /** 48 * Notified when a tap occurs with the down {@link MotionEvent} 49 * that triggered it. This will be triggered immediately for 50 * every down event. All other events should be preceded by this. 51 * 52 * @param e The down motion event. 53 */ 54 boolean onDown(MotionEvent e); 55 56 /** 57 * The user has performed a down {@link MotionEvent} and not performed 58 * a move or up yet. This event is commonly used to provide visual 59 * feedback to the user to let them know that their action has been 60 * recognized i.e. highlight an element. 61 * 62 * @param e The down motion event 63 */ 64 void onShowPress(MotionEvent e); 65 66 /** 67 * Notified when a tap occurs with the up {@link MotionEvent} 68 * that triggered it. 69 * 70 * @param e The up motion event that completed the first tap 71 * @return true if the event is consumed, else false 72 */ 73 boolean onSingleTapUp(MotionEvent e); 74 75 /** 76 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the 77 * current move {@link MotionEvent}. The distance in x and y is also supplied for 78 * convenience. 79 * 80 * @param e1 The first down motion event that started the scrolling. 81 * @param e2 The move motion event that triggered the current onScroll. 82 * @param distanceX The distance along the X axis that has been scrolled since the last 83 * call to onScroll. This is NOT the distance between {@code e1} 84 * and {@code e2}. 85 * @param distanceY The distance along the Y axis that has been scrolled since the last 86 * call to onScroll. This is NOT the distance between {@code e1} 87 * and {@code e2}. 88 * @return true if the event is consumed, else false 89 */ 90 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); 91 92 /** 93 * Notified when a long press occurs with the initial on down {@link MotionEvent} 94 * that trigged it. 95 * 96 * @param e The initial on down motion event that started the longpress. 97 */ 98 void onLongPress(MotionEvent e); 99 100 /** 101 * Notified of a fling event when it occurs with the initial on down {@link MotionEvent} 102 * and the matching up {@link MotionEvent}. The calculated velocity is supplied along 103 * the x and y axis in pixels per second. 104 * 105 * @param e1 The first down motion event that started the fling. 106 * @param e2 The move motion event that triggered the current onFling. 107 * @param velocityX The velocity of this fling measured in pixels per second 108 * along the x axis. 109 * @param velocityY The velocity of this fling measured in pixels per second 110 * along the y axis. 111 * @return true if the event is consumed, else false 112 */ 113 boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); 114 } 115 116 /** 117 * The listener that is used to notify when a double-tap or a confirmed 118 * single-tap occur. 119 */ 120 public interface OnDoubleTapListener { 121 /** 122 * Notified when a single-tap occurs. 123 * <p> 124 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this 125 * will only be called after the detector is confident that the user's 126 * first tap is not followed by a second tap leading to a double-tap 127 * gesture. 128 * 129 * @param e The down motion event of the single-tap. 130 * @return true if the event is consumed, else false 131 */ 132 boolean onSingleTapConfirmed(MotionEvent e); 133 134 /** 135 * Notified when a double-tap occurs. 136 * 137 * @param e The down motion event of the first tap of the double-tap. 138 * @return true if the event is consumed, else false 139 */ 140 boolean onDoubleTap(MotionEvent e); 141 142 /** 143 * Notified when an event within a double-tap gesture occurs, including 144 * the down, move, and up events. 145 * 146 * @param e The motion event that occurred during the double-tap gesture. 147 * @return true if the event is consumed, else false 148 */ 149 boolean onDoubleTapEvent(MotionEvent e); 150 } 151 152 /** 153 * A convenience class to extend when you only want to listen for a subset 154 * of all the gestures. This implements all methods in the 155 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does 156 * nothing and return {@code false} for all applicable methods. 157 */ 158 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener { 159 public boolean onSingleTapUp(MotionEvent e) { 160 return false; 161 } 162 163 public void onLongPress(MotionEvent e) { 164 } 165 166 public boolean onScroll(MotionEvent e1, MotionEvent e2, 167 float distanceX, float distanceY) { 168 return false; 169 } 170 171 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 172 float velocityY) { 173 return false; 174 } 175 176 public void onShowPress(MotionEvent e) { 177 } 178 179 public boolean onDown(MotionEvent e) { 180 return false; 181 } 182 183 public boolean onDoubleTap(MotionEvent e) { 184 return false; 185 } 186 187 public boolean onDoubleTapEvent(MotionEvent e) { 188 return false; 189 } 190 191 public boolean onSingleTapConfirmed(MotionEvent e) { 192 return false; 193 } 194 } 195 196 private int mTouchSlopSquare; 197 private int mDoubleTapTouchSlopSquare; 198 private int mDoubleTapSlopSquare; 199 private int mMinimumFlingVelocity; 200 private int mMaximumFlingVelocity; 201 202 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 203 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); 204 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 mLastFocusX; 230 private float mLastFocusY; 231 private float mDownFocusX; 232 private float mDownFocusY; 233 234 private boolean mIsLongpressEnabled; 235 236 /** 237 * Determines speed during touch scrolling 238 */ 239 private VelocityTracker mVelocityTracker; 240 241 /** 242 * Consistency verifier for debugging purposes. 243 */ 244 private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = 245 InputEventConsistencyVerifier.isInstrumentationEnabled() ? 246 new InputEventConsistencyVerifier(this, 0) : null; 247 248 private class GestureHandler extends Handler { 249 GestureHandler() { 250 super(); 251 } 252 253 GestureHandler(Handler handler) { 254 super(handler.getLooper()); 255 } 256 257 @Override 258 public void handleMessage(Message msg) { 259 switch (msg.what) { 260 case SHOW_PRESS: 261 mListener.onShowPress(mCurrentDownEvent); 262 break; 263 264 case LONG_PRESS: 265 dispatchLongPress(); 266 break; 267 268 case TAP: 269 // If the user's finger is still down, do not count it as a tap 270 if (mDoubleTapListener != null && !mStillDown) { 271 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); 272 } 273 break; 274 275 default: 276 throw new RuntimeException("Unknown message " + msg); //never 277 } 278 } 279 } 280 281 /** 282 * Creates a GestureDetector with the supplied listener. 283 * This variant of the constructor should be used from a non-UI thread 284 * (as it allows specifying the Handler). 285 * 286 * @param listener the listener invoked for all the callbacks, this must 287 * not be null. 288 * @param handler the handler to use 289 * 290 * @throws NullPointerException if either {@code listener} or 291 * {@code handler} is null. 292 * 293 * @deprecated Use {@link #GestureDetector(android.content.Context, 294 * android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead. 295 */ 296 @Deprecated 297 public GestureDetector(OnGestureListener listener, Handler handler) { 298 this(null, listener, handler); 299 } 300 301 /** 302 * Creates a GestureDetector with the supplied listener. 303 * You may only use this constructor from a UI thread (this is the usual situation). 304 * @see android.os.Handler#Handler() 305 * 306 * @param listener the listener invoked for all the callbacks, this must 307 * not be null. 308 * 309 * @throws NullPointerException if {@code listener} is null. 310 * 311 * @deprecated Use {@link #GestureDetector(android.content.Context, 312 * android.view.GestureDetector.OnGestureListener)} instead. 313 */ 314 @Deprecated 315 public GestureDetector(OnGestureListener listener) { 316 this(null, listener, null); 317 } 318 319 /** 320 * Creates a GestureDetector with the supplied listener. 321 * You may only use this constructor from a UI thread (this is the usual situation). 322 * @see android.os.Handler#Handler() 323 * 324 * @param context the application's context 325 * @param listener the listener invoked for all the callbacks, this must 326 * not be null. 327 * 328 * @throws NullPointerException if {@code listener} is null. 329 */ 330 public GestureDetector(Context context, OnGestureListener listener) { 331 this(context, listener, null); 332 } 333 334 /** 335 * Creates a GestureDetector with the supplied listener. 336 * You may only use this constructor from a UI thread (this is the usual situation). 337 * @see android.os.Handler#Handler() 338 * 339 * @param context the application's context 340 * @param listener the listener invoked for all the callbacks, this must 341 * not be null. 342 * @param handler the handler to use 343 * 344 * @throws NullPointerException if {@code listener} is null. 345 */ 346 public GestureDetector(Context context, OnGestureListener listener, Handler handler) { 347 if (handler != null) { 348 mHandler = new GestureHandler(handler); 349 } else { 350 mHandler = new GestureHandler(); 351 } 352 mListener = listener; 353 if (listener instanceof OnDoubleTapListener) { 354 setOnDoubleTapListener((OnDoubleTapListener) listener); 355 } 356 init(context); 357 } 358 359 /** 360 * Creates a GestureDetector with the supplied listener. 361 * You may only use this constructor from a UI thread (this is the usual situation). 362 * @see android.os.Handler#Handler() 363 * 364 * @param context the application's context 365 * @param listener the listener invoked for all the callbacks, this must 366 * not be null. 367 * @param handler the handler to use 368 * 369 * @throws NullPointerException if {@code listener} is null. 370 */ 371 public GestureDetector(Context context, OnGestureListener listener, Handler handler, 372 boolean unused) { 373 this(context, listener, handler); 374 } 375 376 private void init(Context context) { 377 if (mListener == null) { 378 throw new NullPointerException("OnGestureListener must not be null"); 379 } 380 mIsLongpressEnabled = true; 381 382 // Fallback to support pre-donuts releases 383 int touchSlop, doubleTapSlop, doubleTapTouchSlop; 384 if (context == null) { 385 //noinspection deprecation 386 touchSlop = ViewConfiguration.getTouchSlop(); 387 doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this 388 doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); 389 //noinspection deprecation 390 mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); 391 mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); 392 } else { 393 final ViewConfiguration configuration = ViewConfiguration.get(context); 394 touchSlop = configuration.getScaledTouchSlop(); 395 doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop(); 396 doubleTapSlop = configuration.getScaledDoubleTapSlop(); 397 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 398 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); 399 } 400 mTouchSlopSquare = touchSlop * touchSlop; 401 mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop; 402 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; 403 } 404 405 /** 406 * Sets the listener which will be called for double-tap and related 407 * gestures. 408 * 409 * @param onDoubleTapListener the listener invoked for all the callbacks, or 410 * null to stop listening for double-tap gestures. 411 */ 412 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { 413 mDoubleTapListener = onDoubleTapListener; 414 } 415 416 /** 417 * Set whether longpress is enabled, if this is enabled when a user 418 * presses and holds down you get a longpress event and nothing further. 419 * If it's disabled the user can press and hold down and then later 420 * moved their finger and you will get scroll events. By default 421 * longpress is enabled. 422 * 423 * @param isLongpressEnabled whether longpress should be enabled. 424 */ 425 public void setIsLongpressEnabled(boolean isLongpressEnabled) { 426 mIsLongpressEnabled = isLongpressEnabled; 427 } 428 429 /** 430 * @return true if longpress is enabled, else false. 431 */ 432 public boolean isLongpressEnabled() { 433 return mIsLongpressEnabled; 434 } 435 436 /** 437 * Analyzes the given motion event and if applicable triggers the 438 * appropriate callbacks on the {@link OnGestureListener} supplied. 439 * 440 * @param ev The current motion event. 441 * @return true if the {@link OnGestureListener} consumed the event, 442 * else false. 443 */ 444 public boolean onTouchEvent(MotionEvent ev) { 445 if (mInputEventConsistencyVerifier != null) { 446 mInputEventConsistencyVerifier.onTouchEvent(ev, 0); 447 } 448 449 final int action = ev.getAction(); 450 451 if (mVelocityTracker == null) { 452 mVelocityTracker = VelocityTracker.obtain(); 453 } 454 mVelocityTracker.addMovement(ev); 455 456 final boolean pointerUp = 457 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP; 458 final int skipIndex = pointerUp ? ev.getActionIndex() : -1; 459 460 // Determine focal point 461 float sumX = 0, sumY = 0; 462 final int count = ev.getPointerCount(); 463 for (int i = 0; i < count; i++) { 464 if (skipIndex == i) continue; 465 sumX += ev.getX(i); 466 sumY += ev.getY(i); 467 } 468 final int div = pointerUp ? count - 1 : count; 469 final float focusX = sumX / div; 470 final float focusY = sumY / div; 471 472 boolean handled = false; 473 474 switch (action & MotionEvent.ACTION_MASK) { 475 case MotionEvent.ACTION_POINTER_DOWN: 476 mDownFocusX = mLastFocusX = focusX; 477 mDownFocusY = mLastFocusY = focusY; 478 // Cancel long press and taps 479 cancelTaps(); 480 break; 481 482 case MotionEvent.ACTION_POINTER_UP: 483 mDownFocusX = mLastFocusX = focusX; 484 mDownFocusY = mLastFocusY = focusY; 485 break; 486 487 case MotionEvent.ACTION_DOWN: 488 if (mDoubleTapListener != null) { 489 boolean hadTapMessage = mHandler.hasMessages(TAP); 490 if (hadTapMessage) mHandler.removeMessages(TAP); 491 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && 492 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { 493 // This is a second tap 494 mIsDoubleTapping = true; 495 // Give a callback with the first tap of the double-tap 496 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); 497 // Give a callback with down event of the double-tap 498 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 499 } else { 500 // This is a first tap 501 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); 502 } 503 } 504 505 mDownFocusX = mLastFocusX = focusX; 506 mDownFocusY = mLastFocusY = focusY; 507 if (mCurrentDownEvent != null) { 508 mCurrentDownEvent.recycle(); 509 } 510 mCurrentDownEvent = MotionEvent.obtain(ev); 511 mAlwaysInTapRegion = true; 512 mAlwaysInBiggerTapRegion = true; 513 mStillDown = true; 514 mInLongPress = false; 515 516 if (mIsLongpressEnabled) { 517 mHandler.removeMessages(LONG_PRESS); 518 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() 519 + TAP_TIMEOUT + LONGPRESS_TIMEOUT); 520 } 521 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); 522 handled |= mListener.onDown(ev); 523 break; 524 525 case MotionEvent.ACTION_MOVE: 526 if (mInLongPress) { 527 break; 528 } 529 final float scrollX = mLastFocusX - focusX; 530 final float scrollY = mLastFocusY - focusY; 531 if (mIsDoubleTapping) { 532 // Give the move events of the double-tap 533 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 534 } else if (mAlwaysInTapRegion) { 535 final int deltaX = (int) (focusX - mDownFocusX); 536 final int deltaY = (int) (focusY - mDownFocusY); 537 int distance = (deltaX * deltaX) + (deltaY * deltaY); 538 if (distance > mTouchSlopSquare) { 539 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 540 mLastFocusX = focusX; 541 mLastFocusY = focusY; 542 mAlwaysInTapRegion = false; 543 mHandler.removeMessages(TAP); 544 mHandler.removeMessages(SHOW_PRESS); 545 mHandler.removeMessages(LONG_PRESS); 546 } 547 if (distance > mDoubleTapTouchSlopSquare) { 548 mAlwaysInBiggerTapRegion = false; 549 } 550 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { 551 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 552 mLastFocusX = focusX; 553 mLastFocusY = focusY; 554 } 555 break; 556 557 case MotionEvent.ACTION_UP: 558 mStillDown = false; 559 MotionEvent currentUpEvent = MotionEvent.obtain(ev); 560 if (mIsDoubleTapping) { 561 // Finally, give the up event of the double-tap 562 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 563 } else if (mInLongPress) { 564 mHandler.removeMessages(TAP); 565 mInLongPress = false; 566 } else if (mAlwaysInTapRegion) { 567 handled = mListener.onSingleTapUp(ev); 568 } else { 569 570 // A fling must travel the minimum tap distance 571 final VelocityTracker velocityTracker = mVelocityTracker; 572 final int pointerId = ev.getPointerId(0); 573 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 574 final float velocityY = velocityTracker.getYVelocity(pointerId); 575 final float velocityX = velocityTracker.getXVelocity(pointerId); 576 577 if ((Math.abs(velocityY) > mMinimumFlingVelocity) 578 || (Math.abs(velocityX) > mMinimumFlingVelocity)){ 579 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); 580 } 581 } 582 if (mPreviousUpEvent != null) { 583 mPreviousUpEvent.recycle(); 584 } 585 // Hold the event we obtained above - listeners may have changed the original. 586 mPreviousUpEvent = currentUpEvent; 587 if (mVelocityTracker != null) { 588 // This may have been cleared when we called out to the 589 // application above. 590 mVelocityTracker.recycle(); 591 mVelocityTracker = null; 592 } 593 mIsDoubleTapping = false; 594 mHandler.removeMessages(SHOW_PRESS); 595 mHandler.removeMessages(LONG_PRESS); 596 break; 597 598 case MotionEvent.ACTION_CANCEL: 599 cancel(); 600 break; 601 } 602 603 if (!handled && mInputEventConsistencyVerifier != null) { 604 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0); 605 } 606 return handled; 607 } 608 609 private void cancel() { 610 mHandler.removeMessages(SHOW_PRESS); 611 mHandler.removeMessages(LONG_PRESS); 612 mHandler.removeMessages(TAP); 613 mVelocityTracker.recycle(); 614 mVelocityTracker = null; 615 mIsDoubleTapping = false; 616 mStillDown = false; 617 mAlwaysInTapRegion = false; 618 mAlwaysInBiggerTapRegion = false; 619 if (mInLongPress) { 620 mInLongPress = false; 621 } 622 } 623 624 private void cancelTaps() { 625 mHandler.removeMessages(SHOW_PRESS); 626 mHandler.removeMessages(LONG_PRESS); 627 mHandler.removeMessages(TAP); 628 mIsDoubleTapping = false; 629 mAlwaysInTapRegion = false; 630 mAlwaysInBiggerTapRegion = false; 631 if (mInLongPress) { 632 mInLongPress = false; 633 } 634 } 635 636 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, 637 MotionEvent secondDown) { 638 if (!mAlwaysInBiggerTapRegion) { 639 return false; 640 } 641 642 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { 643 return false; 644 } 645 646 int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); 647 int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); 648 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); 649 } 650 651 private void dispatchLongPress() { 652 mHandler.removeMessages(TAP); 653 mInLongPress = true; 654 mListener.onLongPress(mCurrentDownEvent); 655 } 656} 657