1 /* 2 * Copyright (C) 2012 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.support.v4.view; 18 19 import android.content.Context; 20 import android.os.Build; 21 import android.os.Handler; 22 import android.os.Message; 23 import android.view.GestureDetector; 24 import android.view.GestureDetector.OnDoubleTapListener; 25 import android.view.GestureDetector.OnGestureListener; 26 import android.view.MotionEvent; 27 import android.view.VelocityTracker; 28 import android.view.View; 29 import android.view.ViewConfiguration; 30 31 /** 32 * Detects various gestures and events using the supplied {@link MotionEvent}s. 33 * The {@link OnGestureListener} callback will notify users when a particular 34 * motion event has occurred. This class should only be used with {@link MotionEvent}s 35 * reported via touch (don't use for trackball events). 36 * 37 * <p>This compatibility implementation of the framework's GestureDetector guarantees 38 * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.</p> 39 * 40 * To use this class: 41 * <ul> 42 * <li>Create an instance of the {@code GestureDetectorCompat} for your {@link View} 43 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 44 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback 45 * will be executed when the events occur. 46 * </ul> 47 */ 48 public final class GestureDetectorCompat { 49 interface GestureDetectorCompatImpl { 50 boolean isLongpressEnabled(); 51 boolean onTouchEvent(MotionEvent ev); 52 void setIsLongpressEnabled(boolean enabled); 53 void setOnDoubleTapListener(OnDoubleTapListener listener); 54 } 55 56 static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl { 57 private int mTouchSlopSquare; 58 private int mDoubleTapSlopSquare; 59 private int mMinimumFlingVelocity; 60 private int mMaximumFlingVelocity; 61 62 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 63 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); 64 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); 65 66 // constants for Message.what used by GestureHandler below 67 private static final int SHOW_PRESS = 1; 68 private static final int LONG_PRESS = 2; 69 private static final int TAP = 3; 70 71 private final Handler mHandler; 72 final OnGestureListener mListener; 73 OnDoubleTapListener mDoubleTapListener; 74 75 boolean mStillDown; 76 boolean mDeferConfirmSingleTap; 77 private boolean mInLongPress; 78 private boolean mAlwaysInTapRegion; 79 private boolean mAlwaysInBiggerTapRegion; 80 81 MotionEvent mCurrentDownEvent; 82 private MotionEvent mPreviousUpEvent; 83 84 /** 85 * True when the user is still touching for the second tap (down, move, and 86 * up events). Can only be true if there is a double tap listener attached. 87 */ 88 private boolean mIsDoubleTapping; 89 90 private float mLastFocusX; 91 private float mLastFocusY; 92 private float mDownFocusX; 93 private float mDownFocusY; 94 95 private boolean mIsLongpressEnabled; 96 97 /** 98 * Determines speed during touch scrolling 99 */ 100 private VelocityTracker mVelocityTracker; 101 102 private class GestureHandler extends Handler { 103 GestureHandler() { 104 super(); 105 } 106 107 GestureHandler(Handler handler) { 108 super(handler.getLooper()); 109 } 110 111 @Override 112 public void handleMessage(Message msg) { 113 switch (msg.what) { 114 case SHOW_PRESS: 115 mListener.onShowPress(mCurrentDownEvent); 116 break; 117 118 case LONG_PRESS: 119 dispatchLongPress(); 120 break; 121 122 case TAP: 123 // If the user's finger is still down, do not count it as a tap 124 if (mDoubleTapListener != null) { 125 if (!mStillDown) { 126 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); 127 } else { 128 mDeferConfirmSingleTap = true; 129 } 130 } 131 break; 132 133 default: 134 throw new RuntimeException("Unknown message " + msg); //never 135 } 136 } 137 } 138 139 /** 140 * Creates a GestureDetector with the supplied listener. 141 * You may only use this constructor from a UI thread (this is the usual situation). 142 * @see android.os.Handler#Handler() 143 * 144 * @param context the application's context 145 * @param listener the listener invoked for all the callbacks, this must 146 * not be null. 147 * @param handler the handler to use 148 * 149 * @throws NullPointerException if {@code listener} is null. 150 */ 151 public GestureDetectorCompatImplBase(Context context, OnGestureListener listener, 152 Handler handler) { 153 if (handler != null) { 154 mHandler = new GestureHandler(handler); 155 } else { 156 mHandler = new GestureHandler(); 157 } 158 mListener = listener; 159 if (listener instanceof OnDoubleTapListener) { 160 setOnDoubleTapListener((OnDoubleTapListener) listener); 161 } 162 init(context); 163 } 164 165 private void init(Context context) { 166 if (context == null) { 167 throw new IllegalArgumentException("Context must not be null"); 168 } 169 if (mListener == null) { 170 throw new IllegalArgumentException("OnGestureListener must not be null"); 171 } 172 mIsLongpressEnabled = true; 173 174 final ViewConfiguration configuration = ViewConfiguration.get(context); 175 final int touchSlop = configuration.getScaledTouchSlop(); 176 final int doubleTapSlop = configuration.getScaledDoubleTapSlop(); 177 mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); 178 mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); 179 180 mTouchSlopSquare = touchSlop * touchSlop; 181 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; 182 } 183 184 /** 185 * Sets the listener which will be called for double-tap and related 186 * gestures. 187 * 188 * @param onDoubleTapListener the listener invoked for all the callbacks, or 189 * null to stop listening for double-tap gestures. 190 */ 191 @Override 192 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { 193 mDoubleTapListener = onDoubleTapListener; 194 } 195 196 /** 197 * Set whether longpress is enabled, if this is enabled when a user 198 * presses and holds down you get a longpress event and nothing further. 199 * If it's disabled the user can press and hold down and then later 200 * moved their finger and you will get scroll events. By default 201 * longpress is enabled. 202 * 203 * @param isLongpressEnabled whether longpress should be enabled. 204 */ 205 @Override 206 public void setIsLongpressEnabled(boolean isLongpressEnabled) { 207 mIsLongpressEnabled = isLongpressEnabled; 208 } 209 210 /** 211 * @return true if longpress is enabled, else false. 212 */ 213 @Override 214 public boolean isLongpressEnabled() { 215 return mIsLongpressEnabled; 216 } 217 218 /** 219 * Analyzes the given motion event and if applicable triggers the 220 * appropriate callbacks on the {@link OnGestureListener} supplied. 221 * 222 * @param ev The current motion event. 223 * @return true if the {@link OnGestureListener} consumed the event, 224 * else false. 225 */ 226 @Override 227 public boolean onTouchEvent(MotionEvent ev) { 228 final int action = ev.getAction(); 229 230 if (mVelocityTracker == null) { 231 mVelocityTracker = VelocityTracker.obtain(); 232 } 233 mVelocityTracker.addMovement(ev); 234 235 final boolean pointerUp = 236 (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP; 237 final int skipIndex = pointerUp ? ev.getActionIndex() : -1; 238 239 // Determine focal point 240 float sumX = 0, sumY = 0; 241 final int count = ev.getPointerCount(); 242 for (int i = 0; i < count; i++) { 243 if (skipIndex == i) continue; 244 sumX += ev.getX(i); 245 sumY += ev.getY(i); 246 } 247 final int div = pointerUp ? count - 1 : count; 248 final float focusX = sumX / div; 249 final float focusY = sumY / div; 250 251 boolean handled = false; 252 253 switch (action & MotionEvent.ACTION_MASK) { 254 case MotionEvent.ACTION_POINTER_DOWN: 255 mDownFocusX = mLastFocusX = focusX; 256 mDownFocusY = mLastFocusY = focusY; 257 // Cancel long press and taps 258 cancelTaps(); 259 break; 260 261 case MotionEvent.ACTION_POINTER_UP: 262 mDownFocusX = mLastFocusX = focusX; 263 mDownFocusY = mLastFocusY = focusY; 264 265 // Check the dot product of current velocities. 266 // If the pointer that left was opposing another velocity vector, clear. 267 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 268 final int upIndex = ev.getActionIndex(); 269 final int id1 = ev.getPointerId(upIndex); 270 final float x1 = mVelocityTracker.getXVelocity(id1); 271 final float y1 = mVelocityTracker.getYVelocity(id1); 272 for (int i = 0; i < count; i++) { 273 if (i == upIndex) continue; 274 275 final int id2 = ev.getPointerId(i); 276 final float x = x1 * mVelocityTracker.getXVelocity(id2); 277 final float y = y1 * mVelocityTracker.getYVelocity(id2); 278 279 final float dot = x + y; 280 if (dot < 0) { 281 mVelocityTracker.clear(); 282 break; 283 } 284 } 285 break; 286 287 case MotionEvent.ACTION_DOWN: 288 if (mDoubleTapListener != null) { 289 boolean hadTapMessage = mHandler.hasMessages(TAP); 290 if (hadTapMessage) mHandler.removeMessages(TAP); 291 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) 292 && hadTapMessage && isConsideredDoubleTap( 293 mCurrentDownEvent, mPreviousUpEvent, ev)) { 294 // This is a second tap 295 mIsDoubleTapping = true; 296 // Give a callback with the first tap of the double-tap 297 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); 298 // Give a callback with down event of the double-tap 299 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 300 } else { 301 // This is a first tap 302 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); 303 } 304 } 305 306 mDownFocusX = mLastFocusX = focusX; 307 mDownFocusY = mLastFocusY = focusY; 308 if (mCurrentDownEvent != null) { 309 mCurrentDownEvent.recycle(); 310 } 311 mCurrentDownEvent = MotionEvent.obtain(ev); 312 mAlwaysInTapRegion = true; 313 mAlwaysInBiggerTapRegion = true; 314 mStillDown = true; 315 mInLongPress = false; 316 mDeferConfirmSingleTap = false; 317 318 if (mIsLongpressEnabled) { 319 mHandler.removeMessages(LONG_PRESS); 320 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() 321 + TAP_TIMEOUT + LONGPRESS_TIMEOUT); 322 } 323 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, 324 mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); 325 handled |= mListener.onDown(ev); 326 break; 327 328 case MotionEvent.ACTION_MOVE: 329 if (mInLongPress) { 330 break; 331 } 332 final float scrollX = mLastFocusX - focusX; 333 final float scrollY = mLastFocusY - focusY; 334 if (mIsDoubleTapping) { 335 // Give the move events of the double-tap 336 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 337 } else if (mAlwaysInTapRegion) { 338 final int deltaX = (int) (focusX - mDownFocusX); 339 final int deltaY = (int) (focusY - mDownFocusY); 340 int distance = (deltaX * deltaX) + (deltaY * deltaY); 341 if (distance > mTouchSlopSquare) { 342 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 343 mLastFocusX = focusX; 344 mLastFocusY = focusY; 345 mAlwaysInTapRegion = false; 346 mHandler.removeMessages(TAP); 347 mHandler.removeMessages(SHOW_PRESS); 348 mHandler.removeMessages(LONG_PRESS); 349 } 350 if (distance > mTouchSlopSquare) { 351 mAlwaysInBiggerTapRegion = false; 352 } 353 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { 354 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 355 mLastFocusX = focusX; 356 mLastFocusY = focusY; 357 } 358 break; 359 360 case MotionEvent.ACTION_UP: 361 mStillDown = false; 362 MotionEvent currentUpEvent = MotionEvent.obtain(ev); 363 if (mIsDoubleTapping) { 364 // Finally, give the up event of the double-tap 365 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 366 } else if (mInLongPress) { 367 mHandler.removeMessages(TAP); 368 mInLongPress = false; 369 } else if (mAlwaysInTapRegion) { 370 handled = mListener.onSingleTapUp(ev); 371 if (mDeferConfirmSingleTap && mDoubleTapListener != null) { 372 mDoubleTapListener.onSingleTapConfirmed(ev); 373 } 374 } else { 375 // A fling must travel the minimum tap distance 376 final VelocityTracker velocityTracker = mVelocityTracker; 377 final int pointerId = ev.getPointerId(0); 378 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 379 final float velocityY = velocityTracker.getYVelocity(pointerId); 380 final float velocityX = velocityTracker.getXVelocity(pointerId); 381 382 if ((Math.abs(velocityY) > mMinimumFlingVelocity) 383 || (Math.abs(velocityX) > mMinimumFlingVelocity)) { 384 handled = mListener.onFling( 385 mCurrentDownEvent, ev, velocityX, velocityY); 386 } 387 } 388 if (mPreviousUpEvent != null) { 389 mPreviousUpEvent.recycle(); 390 } 391 // Hold the event we obtained above - listeners may have changed the original. 392 mPreviousUpEvent = currentUpEvent; 393 if (mVelocityTracker != null) { 394 // This may have been cleared when we called out to the 395 // application above. 396 mVelocityTracker.recycle(); 397 mVelocityTracker = null; 398 } 399 mIsDoubleTapping = false; 400 mDeferConfirmSingleTap = false; 401 mHandler.removeMessages(SHOW_PRESS); 402 mHandler.removeMessages(LONG_PRESS); 403 break; 404 405 case MotionEvent.ACTION_CANCEL: 406 cancel(); 407 break; 408 } 409 410 return handled; 411 } 412 413 private void cancel() { 414 mHandler.removeMessages(SHOW_PRESS); 415 mHandler.removeMessages(LONG_PRESS); 416 mHandler.removeMessages(TAP); 417 mVelocityTracker.recycle(); 418 mVelocityTracker = null; 419 mIsDoubleTapping = false; 420 mStillDown = false; 421 mAlwaysInTapRegion = false; 422 mAlwaysInBiggerTapRegion = false; 423 mDeferConfirmSingleTap = false; 424 if (mInLongPress) { 425 mInLongPress = false; 426 } 427 } 428 429 private void cancelTaps() { 430 mHandler.removeMessages(SHOW_PRESS); 431 mHandler.removeMessages(LONG_PRESS); 432 mHandler.removeMessages(TAP); 433 mIsDoubleTapping = false; 434 mAlwaysInTapRegion = false; 435 mAlwaysInBiggerTapRegion = false; 436 mDeferConfirmSingleTap = false; 437 if (mInLongPress) { 438 mInLongPress = false; 439 } 440 } 441 442 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, 443 MotionEvent secondDown) { 444 if (!mAlwaysInBiggerTapRegion) { 445 return false; 446 } 447 448 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { 449 return false; 450 } 451 452 int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); 453 int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); 454 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); 455 } 456 457 void dispatchLongPress() { 458 mHandler.removeMessages(TAP); 459 mDeferConfirmSingleTap = false; 460 mInLongPress = true; 461 mListener.onLongPress(mCurrentDownEvent); 462 } 463 } 464 465 static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl { 466 private final GestureDetector mDetector; 467 468 public GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener, 469 Handler handler) { 470 mDetector = new GestureDetector(context, listener, handler); 471 } 472 473 @Override 474 public boolean isLongpressEnabled() { 475 return mDetector.isLongpressEnabled(); 476 } 477 478 @Override 479 public boolean onTouchEvent(MotionEvent ev) { 480 return mDetector.onTouchEvent(ev); 481 } 482 483 @Override 484 public void setIsLongpressEnabled(boolean enabled) { 485 mDetector.setIsLongpressEnabled(enabled); 486 } 487 488 @Override 489 public void setOnDoubleTapListener(OnDoubleTapListener listener) { 490 mDetector.setOnDoubleTapListener(listener); 491 } 492 } 493 494 private final GestureDetectorCompatImpl mImpl; 495 496 /** 497 * Creates a GestureDetectorCompat with the supplied listener. 498 * As usual, you may only use this constructor from a UI thread. 499 * @see android.os.Handler#Handler() 500 * 501 * @param context the application's context 502 * @param listener the listener invoked for all the callbacks, this must 503 * not be null. 504 */ 505 public GestureDetectorCompat(Context context, OnGestureListener listener) { 506 this(context, listener, null); 507 } 508 509 /** 510 * Creates a GestureDetectorCompat with the supplied listener. 511 * As usual, you may only use this constructor from a UI thread. 512 * @see android.os.Handler#Handler() 513 * 514 * @param context the application's context 515 * @param listener the listener invoked for all the callbacks, this must 516 * not be null. 517 * @param handler the handler that will be used for posting deferred messages 518 */ 519 public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) { 520 if (Build.VERSION.SDK_INT > 17) { 521 mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler); 522 } else { 523 mImpl = new GestureDetectorCompatImplBase(context, listener, handler); 524 } 525 } 526 527 /** 528 * @return true if longpress is enabled, else false. 529 */ 530 public boolean isLongpressEnabled() { 531 return mImpl.isLongpressEnabled(); 532 } 533 534 /** 535 * Analyzes the given motion event and if applicable triggers the 536 * appropriate callbacks on the {@link OnGestureListener} supplied. 537 * 538 * @param event The current motion event. 539 * @return true if the {@link OnGestureListener} consumed the event, 540 * else false. 541 */ 542 public boolean onTouchEvent(MotionEvent event) { 543 return mImpl.onTouchEvent(event); 544 } 545 546 /** 547 * Set whether longpress is enabled, if this is enabled when a user 548 * presses and holds down you get a longpress event and nothing further. 549 * If it's disabled the user can press and hold down and then later 550 * moved their finger and you will get scroll events. By default 551 * longpress is enabled. 552 * 553 * @param enabled whether longpress should be enabled. 554 */ 555 public void setIsLongpressEnabled(boolean enabled) { 556 mImpl.setIsLongpressEnabled(enabled); 557 } 558 559 /** 560 * Sets the listener which will be called for double-tap and related 561 * gestures. 562 * 563 * @param listener the listener invoked for all the callbacks, or 564 * null to stop listening for double-tap gestures. 565 */ 566 public void setOnDoubleTapListener(OnDoubleTapListener listener) { 567 mImpl.setOnDoubleTapListener(listener); 568 } 569 } 570