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