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