GestureDetectorCompat.java revision 3ac77bf186f87ecad4bf0063b2f6c4384efbd56a
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 17package android.support.v4.view; 18 19import android.content.Context; 20import android.os.Build; 21import android.os.Handler; 22import android.os.Message; 23import android.view.GestureDetector; 24import android.view.GestureDetector.OnDoubleTapListener; 25import android.view.GestureDetector.OnGestureListener; 26import android.view.MotionEvent; 27import android.view.VelocityTracker; 28import android.view.View; 29import 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 */ 48public 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 private final OnGestureListener mListener; 73 private OnDoubleTapListener mDoubleTapListener; 74 75 private boolean mStillDown; 76 private boolean mDeferConfirmSingleTap; 77 private boolean mInLongPress; 78 private boolean mAlwaysInTapRegion; 79 private boolean mAlwaysInBiggerTapRegion; 80 81 private 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 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { 192 mDoubleTapListener = onDoubleTapListener; 193 } 194 195 /** 196 * Set whether longpress is enabled, if this is enabled when a user 197 * presses and holds down you get a longpress event and nothing further. 198 * If it's disabled the user can press and hold down and then later 199 * moved their finger and you will get scroll events. By default 200 * longpress is enabled. 201 * 202 * @param isLongpressEnabled whether longpress should be enabled. 203 */ 204 public void setIsLongpressEnabled(boolean isLongpressEnabled) { 205 mIsLongpressEnabled = isLongpressEnabled; 206 } 207 208 /** 209 * @return true if longpress is enabled, else false. 210 */ 211 public boolean isLongpressEnabled() { 212 return mIsLongpressEnabled; 213 } 214 215 /** 216 * Analyzes the given motion event and if applicable triggers the 217 * appropriate callbacks on the {@link OnGestureListener} supplied. 218 * 219 * @param ev The current motion event. 220 * @return true if the {@link OnGestureListener} consumed the event, 221 * else false. 222 */ 223 public boolean onTouchEvent(MotionEvent ev) { 224 final int action = ev.getAction(); 225 226 if (mVelocityTracker == null) { 227 mVelocityTracker = VelocityTracker.obtain(); 228 } 229 mVelocityTracker.addMovement(ev); 230 231 final boolean pointerUp = 232 (action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP; 233 final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1; 234 235 // Determine focal point 236 float sumX = 0, sumY = 0; 237 final int count = MotionEventCompat.getPointerCount(ev); 238 for (int i = 0; i < count; i++) { 239 if (skipIndex == i) continue; 240 sumX += MotionEventCompat.getX(ev, i); 241 sumY += MotionEventCompat.getY(ev, i); 242 } 243 final int div = pointerUp ? count - 1 : count; 244 final float focusX = sumX / div; 245 final float focusY = sumY / div; 246 247 boolean handled = false; 248 249 switch (action & MotionEventCompat.ACTION_MASK) { 250 case MotionEventCompat.ACTION_POINTER_DOWN: 251 mDownFocusX = mLastFocusX = focusX; 252 mDownFocusY = mLastFocusY = focusY; 253 // Cancel long press and taps 254 cancelTaps(); 255 break; 256 257 case MotionEventCompat.ACTION_POINTER_UP: 258 mDownFocusX = mLastFocusX = focusX; 259 mDownFocusY = mLastFocusY = focusY; 260 261 // Check the dot product of current velocities. 262 // If the pointer that left was opposing another velocity vector, clear. 263 mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 264 final int upIndex = MotionEventCompat.getActionIndex(ev); 265 final int id1 = MotionEventCompat.getPointerId(ev, upIndex); 266 final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1); 267 final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1); 268 for (int i = 0; i < count; i++) { 269 if (i == upIndex) continue; 270 271 final int id2 = MotionEventCompat.getPointerId(ev, i); 272 final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2); 273 final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2); 274 275 final float dot = x + y; 276 if (dot < 0) { 277 mVelocityTracker.clear(); 278 break; 279 } 280 } 281 break; 282 283 case MotionEvent.ACTION_DOWN: 284 if (mDoubleTapListener != null) { 285 boolean hadTapMessage = mHandler.hasMessages(TAP); 286 if (hadTapMessage) mHandler.removeMessages(TAP); 287 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && 288 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { 289 // This is a second tap 290 mIsDoubleTapping = true; 291 // Give a callback with the first tap of the double-tap 292 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); 293 // Give a callback with down event of the double-tap 294 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 295 } else { 296 // This is a first tap 297 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); 298 } 299 } 300 301 mDownFocusX = mLastFocusX = focusX; 302 mDownFocusY = mLastFocusY = focusY; 303 if (mCurrentDownEvent != null) { 304 mCurrentDownEvent.recycle(); 305 } 306 mCurrentDownEvent = MotionEvent.obtain(ev); 307 mAlwaysInTapRegion = true; 308 mAlwaysInBiggerTapRegion = true; 309 mStillDown = true; 310 mInLongPress = false; 311 mDeferConfirmSingleTap = false; 312 313 if (mIsLongpressEnabled) { 314 mHandler.removeMessages(LONG_PRESS); 315 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() 316 + TAP_TIMEOUT + LONGPRESS_TIMEOUT); 317 } 318 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); 319 handled |= mListener.onDown(ev); 320 break; 321 322 case MotionEvent.ACTION_MOVE: 323 if (mInLongPress) { 324 break; 325 } 326 final float scrollX = mLastFocusX - focusX; 327 final float scrollY = mLastFocusY - focusY; 328 if (mIsDoubleTapping) { 329 // Give the move events of the double-tap 330 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 331 } else if (mAlwaysInTapRegion) { 332 final int deltaX = (int) (focusX - mDownFocusX); 333 final int deltaY = (int) (focusY - mDownFocusY); 334 int distance = (deltaX * deltaX) + (deltaY * deltaY); 335 if (distance > mTouchSlopSquare) { 336 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 337 mLastFocusX = focusX; 338 mLastFocusY = focusY; 339 mAlwaysInTapRegion = false; 340 mHandler.removeMessages(TAP); 341 mHandler.removeMessages(SHOW_PRESS); 342 mHandler.removeMessages(LONG_PRESS); 343 } 344 if (distance > mTouchSlopSquare) { 345 mAlwaysInBiggerTapRegion = false; 346 } 347 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { 348 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 349 mLastFocusX = focusX; 350 mLastFocusY = focusY; 351 } 352 break; 353 354 case MotionEvent.ACTION_UP: 355 mStillDown = false; 356 MotionEvent currentUpEvent = MotionEvent.obtain(ev); 357 if (mIsDoubleTapping) { 358 // Finally, give the up event of the double-tap 359 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 360 } else if (mInLongPress) { 361 mHandler.removeMessages(TAP); 362 mInLongPress = false; 363 } else if (mAlwaysInTapRegion) { 364 handled = mListener.onSingleTapUp(ev); 365 if (mDeferConfirmSingleTap && mDoubleTapListener != null) { 366 mDoubleTapListener.onSingleTapConfirmed(ev); 367 } 368 } else { 369 // A fling must travel the minimum tap distance 370 final VelocityTracker velocityTracker = mVelocityTracker; 371 final int pointerId = MotionEventCompat.getPointerId(ev, 0); 372 velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); 373 final float velocityY = VelocityTrackerCompat.getYVelocity( 374 velocityTracker, pointerId); 375 final float velocityX = VelocityTrackerCompat.getXVelocity( 376 velocityTracker, pointerId); 377 378 if ((Math.abs(velocityY) > mMinimumFlingVelocity) 379 || (Math.abs(velocityX) > mMinimumFlingVelocity)){ 380 handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); 381 } 382 } 383 if (mPreviousUpEvent != null) { 384 mPreviousUpEvent.recycle(); 385 } 386 // Hold the event we obtained above - listeners may have changed the original. 387 mPreviousUpEvent = currentUpEvent; 388 if (mVelocityTracker != null) { 389 // This may have been cleared when we called out to the 390 // application above. 391 mVelocityTracker.recycle(); 392 mVelocityTracker = null; 393 } 394 mIsDoubleTapping = false; 395 mDeferConfirmSingleTap = false; 396 mHandler.removeMessages(SHOW_PRESS); 397 mHandler.removeMessages(LONG_PRESS); 398 break; 399 400 case MotionEvent.ACTION_CANCEL: 401 cancel(); 402 break; 403 } 404 405 return handled; 406 } 407 408 private void cancel() { 409 mHandler.removeMessages(SHOW_PRESS); 410 mHandler.removeMessages(LONG_PRESS); 411 mHandler.removeMessages(TAP); 412 mVelocityTracker.recycle(); 413 mVelocityTracker = null; 414 mIsDoubleTapping = false; 415 mStillDown = false; 416 mAlwaysInTapRegion = false; 417 mAlwaysInBiggerTapRegion = false; 418 mDeferConfirmSingleTap = false; 419 if (mInLongPress) { 420 mInLongPress = false; 421 } 422 } 423 424 private void cancelTaps() { 425 mHandler.removeMessages(SHOW_PRESS); 426 mHandler.removeMessages(LONG_PRESS); 427 mHandler.removeMessages(TAP); 428 mIsDoubleTapping = false; 429 mAlwaysInTapRegion = false; 430 mAlwaysInBiggerTapRegion = false; 431 mDeferConfirmSingleTap = false; 432 if (mInLongPress) { 433 mInLongPress = false; 434 } 435 } 436 437 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, 438 MotionEvent secondDown) { 439 if (!mAlwaysInBiggerTapRegion) { 440 return false; 441 } 442 443 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { 444 return false; 445 } 446 447 int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); 448 int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); 449 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); 450 } 451 452 private void dispatchLongPress() { 453 mHandler.removeMessages(TAP); 454 mDeferConfirmSingleTap = false; 455 mInLongPress = true; 456 mListener.onLongPress(mCurrentDownEvent); 457 } 458 } 459 460 static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl { 461 private final GestureDetector mDetector; 462 463 public GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener, 464 Handler handler) { 465 mDetector = new GestureDetector(context, listener, handler); 466 } 467 468 @Override 469 public boolean isLongpressEnabled() { 470 return mDetector.isLongpressEnabled(); 471 } 472 473 @Override 474 public boolean onTouchEvent(MotionEvent ev) { 475 return mDetector.onTouchEvent(ev); 476 } 477 478 @Override 479 public void setIsLongpressEnabled(boolean enabled) { 480 mDetector.setIsLongpressEnabled(enabled); 481 } 482 483 @Override 484 public void setOnDoubleTapListener(OnDoubleTapListener listener) { 485 mDetector.setOnDoubleTapListener(listener); 486 } 487 } 488 489 private final GestureDetectorCompatImpl mImpl; 490 491 /** 492 * Creates a GestureDetectorCompat with the supplied listener. 493 * As usual, you may only use this constructor from a UI thread. 494 * @see android.os.Handler#Handler() 495 * 496 * @param context the application's context 497 * @param listener the listener invoked for all the callbacks, this must 498 * not be null. 499 */ 500 public GestureDetectorCompat(Context context, OnGestureListener listener) { 501 this(context, listener, null); 502 } 503 504 /** 505 * Creates a GestureDetectorCompat with the supplied listener. 506 * As usual, you may only use this constructor from a UI thread. 507 * @see android.os.Handler#Handler() 508 * 509 * @param context the application's context 510 * @param listener the listener invoked for all the callbacks, this must 511 * not be null. 512 * @param handler the handler that will be used for posting deferred messages 513 */ 514 public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) { 515 if (Build.VERSION.SDK_INT > 17) { 516 mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler); 517 } else { 518 mImpl = new GestureDetectorCompatImplBase(context, listener, handler); 519 } 520 } 521 522 /** 523 * @return true if longpress is enabled, else false. 524 */ 525 public boolean isLongpressEnabled() { 526 return mImpl.isLongpressEnabled(); 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 event The current motion event. 534 * @return true if the {@link OnGestureListener} consumed the event, 535 * else false. 536 */ 537 public boolean onTouchEvent(MotionEvent event) { 538 return mImpl.onTouchEvent(event); 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 enabled whether longpress should be enabled. 549 */ 550 public void setIsLongpressEnabled(boolean enabled) { 551 mImpl.setIsLongpressEnabled(enabled); 552 } 553 554 /** 555 * Sets the listener which will be called for double-tap and related 556 * gestures. 557 * 558 * @param listener the listener invoked for all the callbacks, or 559 * null to stop listening for double-tap gestures. 560 */ 561 public void setOnDoubleTapListener(OnDoubleTapListener listener) { 562 mImpl.setOnDoubleTapListener(listener); 563 } 564} 565