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