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