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