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