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 * Modified for aFreeRDP by Martin Fleisz (martin.fleisz@thincast.com) 17 */ 18 19 package com.freerdp.freerdpcore.utils; 20 21 import android.content.Context; 22 import android.os.Build; 23 import android.os.Handler; 24 import android.os.Message; 25 import android.util.DisplayMetrics; 26 import android.view.MotionEvent; 27 import android.view.ViewConfiguration; 28 29 public class GestureDetector 30 { 31 32 private static final int TAP_TIMEOUT = 100; 33 private static final int DOUBLE_TAP_TIMEOUT = 200; 34 // Distance a touch can wander before we think the user is the first touch in a sequence of 35 // double tap 36 private static final int LARGE_TOUCH_SLOP = 18; 37 // Distance between the first touch and second touch to still be considered a double tap 38 private static final int DOUBLE_TAP_SLOP = 100; 39 // constants for Message.what used by GestureHandler below 40 private static final int SHOW_PRESS = 1; 41 private static final int LONG_PRESS = 2; 42 private static final int TAP = 3; 43 private final Handler mHandler; 44 private final OnGestureListener mListener; 45 private int mTouchSlopSquare; 46 private int mLargeTouchSlopSquare; 47 private int mDoubleTapSlopSquare; 48 private int mLongpressTimeout = 100; 49 private OnDoubleTapListener mDoubleTapListener; 50 private boolean mStillDown; 51 private boolean mInLongPress; 52 private boolean mAlwaysInTapRegion; 53 private boolean mAlwaysInBiggerTapRegion; 54 private MotionEvent mCurrentDownEvent; 55 private MotionEvent mPreviousUpEvent; 56 /** 57 * True when the user is still touching for the second tap (down, move, and 58 * up events). Can only be true if there is a double tap listener attached. 59 */ 60 private boolean mIsDoubleTapping; 61 private float mLastMotionY; 62 private float mLastMotionX; 63 private boolean mIsLongpressEnabled; 64 /** 65 * True if we are at a target API level of >= Froyo or the developer can 66 * explicitly set it. If true, input events with > 1 pointer will be ignored 67 * so we can work side by side with multitouch gesture detectors. 68 */ 69 private boolean mIgnoreMultitouch; 70 /** 71 * Creates a GestureDetector with the supplied listener. 72 * You may only use this constructor from a UI thread (this is the usual situation). 73 * 74 * @param context the application's context 75 * @param listener the listener invoked for all the callbacks, this must 76 * not be null. 77 * @throws NullPointerException if {@code listener} is null. 78 * @see android.os.Handler#Handler() 79 */ GestureDetector(Context context, OnGestureListener listener)80 public GestureDetector(Context context, OnGestureListener listener) 81 { 82 this(context, listener, null); 83 } 84 85 /** 86 * Creates a GestureDetector with the supplied listener. 87 * You may only use this constructor from a UI thread (this is the usual situation). 88 * 89 * @param context the application's context 90 * @param listener the listener invoked for all the callbacks, this must 91 * not be null. 92 * @param handler the handler to use 93 * @throws NullPointerException if {@code listener} is null. 94 * @see android.os.Handler#Handler() 95 */ GestureDetector(Context context, OnGestureListener listener, Handler handler)96 public GestureDetector(Context context, OnGestureListener listener, Handler handler) 97 { 98 this(context, listener, handler, 99 context != null && 100 context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO); 101 } 102 103 /** 104 * Creates a GestureDetector with the supplied listener. 105 * You may only use this constructor from a UI thread (this is the usual situation). 106 * 107 * @param context the application's context 108 * @param listener the listener invoked for all the callbacks, this must 109 * not be null. 110 * @param handler the handler to use 111 * @param ignoreMultitouch whether events involving more than one pointer should 112 * be ignored. 113 * @throws NullPointerException if {@code listener} is null. 114 * @see android.os.Handler#Handler() 115 */ GestureDetector(Context context, OnGestureListener listener, Handler handler, boolean ignoreMultitouch)116 public GestureDetector(Context context, OnGestureListener listener, Handler handler, 117 boolean ignoreMultitouch) 118 { 119 if (handler != null) 120 { 121 mHandler = new GestureHandler(handler); 122 } 123 else 124 { 125 mHandler = new GestureHandler(); 126 } 127 mListener = listener; 128 if (listener instanceof OnDoubleTapListener) 129 { 130 setOnDoubleTapListener((OnDoubleTapListener)listener); 131 } 132 init(context, ignoreMultitouch); 133 } 134 init(Context context, boolean ignoreMultitouch)135 private void init(Context context, boolean ignoreMultitouch) 136 { 137 if (mListener == null) 138 { 139 throw new NullPointerException("OnGestureListener must not be null"); 140 } 141 mIsLongpressEnabled = true; 142 mIgnoreMultitouch = ignoreMultitouch; 143 144 // Fallback to support pre-donuts releases 145 int touchSlop, largeTouchSlop, doubleTapSlop; 146 if (context == null) 147 { 148 // noinspection deprecation 149 touchSlop = ViewConfiguration.getTouchSlop(); 150 largeTouchSlop = touchSlop + 2; 151 doubleTapSlop = DOUBLE_TAP_SLOP; 152 } 153 else 154 { 155 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 156 final float density = metrics.density; 157 final ViewConfiguration configuration = ViewConfiguration.get(context); 158 touchSlop = configuration.getScaledTouchSlop(); 159 largeTouchSlop = (int)(density * LARGE_TOUCH_SLOP + 0.5f); 160 doubleTapSlop = configuration.getScaledDoubleTapSlop(); 161 } 162 mTouchSlopSquare = touchSlop * touchSlop; 163 mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop; 164 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; 165 } 166 167 /** 168 * Sets the listener which will be called for double-tap and related 169 * gestures. 170 * 171 * @param onDoubleTapListener the listener invoked for all the callbacks, or 172 * null to stop listening for double-tap gestures. 173 */ setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)174 public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) 175 { 176 mDoubleTapListener = onDoubleTapListener; 177 } 178 179 /** 180 * Set whether longpress is enabled, if this is enabled when a user 181 * presses and holds down you get a longpress event and nothing further. 182 * If it's disabled the user can press and hold down and then later 183 * moved their finger and you will get scroll events. By default 184 * longpress is enabled. 185 * 186 * @param isLongpressEnabled whether longpress should be enabled. 187 */ setIsLongpressEnabled(boolean isLongpressEnabled)188 public void setIsLongpressEnabled(boolean isLongpressEnabled) 189 { 190 mIsLongpressEnabled = isLongpressEnabled; 191 } 192 193 /** 194 * @return true if longpress is enabled, else false. 195 */ isLongpressEnabled()196 public boolean isLongpressEnabled() 197 { 198 return mIsLongpressEnabled; 199 } 200 setLongPressTimeout(int timeout)201 public void setLongPressTimeout(int timeout) 202 { 203 mLongpressTimeout = timeout; 204 } 205 206 /** 207 * Analyzes the given motion event and if applicable triggers the 208 * appropriate callbacks on the {@link OnGestureListener} supplied. 209 * 210 * @param ev The current motion event. 211 * @return true if the {@link OnGestureListener} consumed the event, 212 * else false. 213 */ onTouchEvent(MotionEvent ev)214 public boolean onTouchEvent(MotionEvent ev) 215 { 216 final int action = ev.getAction(); 217 final float y = ev.getY(); 218 final float x = ev.getX(); 219 220 boolean handled = false; 221 222 switch (action & MotionEvent.ACTION_MASK) 223 { 224 case MotionEvent.ACTION_POINTER_DOWN: 225 if (mIgnoreMultitouch) 226 { 227 // Multitouch event - abort. 228 cancel(); 229 } 230 break; 231 232 case MotionEvent.ACTION_POINTER_UP: 233 // Ending a multitouch gesture and going back to 1 finger 234 if (mIgnoreMultitouch && ev.getPointerCount() == 2) 235 { 236 int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 237 MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) 238 ? 1 239 : 0; 240 mLastMotionX = ev.getX(index); 241 mLastMotionY = ev.getY(index); 242 } 243 break; 244 245 case MotionEvent.ACTION_DOWN: 246 if (mDoubleTapListener != null) 247 { 248 boolean hadTapMessage = mHandler.hasMessages(TAP); 249 if (hadTapMessage) 250 mHandler.removeMessages(TAP); 251 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && 252 hadTapMessage && 253 isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) 254 { 255 // This is a second tap 256 mIsDoubleTapping = true; 257 // Give a callback with the first tap of the double-tap 258 handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); 259 // Give a callback with down event of the double-tap 260 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 261 } 262 else 263 { 264 // This is a first tap 265 mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); 266 } 267 } 268 269 mLastMotionX = x; 270 mLastMotionY = y; 271 if (mCurrentDownEvent != null) 272 { 273 mCurrentDownEvent.recycle(); 274 } 275 mCurrentDownEvent = MotionEvent.obtain(ev); 276 mAlwaysInTapRegion = true; 277 mAlwaysInBiggerTapRegion = true; 278 mStillDown = true; 279 mInLongPress = false; 280 281 if (mIsLongpressEnabled) 282 { 283 mHandler.removeMessages(LONG_PRESS); 284 mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() + 285 TAP_TIMEOUT + 286 mLongpressTimeout); 287 } 288 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, 289 mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); 290 handled |= mListener.onDown(ev); 291 break; 292 293 case MotionEvent.ACTION_MOVE: 294 if (mIgnoreMultitouch && ev.getPointerCount() > 1) 295 { 296 break; 297 } 298 final float scrollX = mLastMotionX - x; 299 final float scrollY = mLastMotionY - y; 300 if (mIsDoubleTapping) 301 { 302 // Give the move events of the double-tap 303 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 304 } 305 else if (mAlwaysInTapRegion) 306 { 307 final int deltaX = (int)(x - mCurrentDownEvent.getX()); 308 final int deltaY = (int)(y - mCurrentDownEvent.getY()); 309 int distance = (deltaX * deltaX) + (deltaY * deltaY); 310 if (distance > mTouchSlopSquare) 311 { 312 mLastMotionX = x; 313 mLastMotionY = y; 314 mAlwaysInTapRegion = false; 315 mHandler.removeMessages(TAP); 316 mHandler.removeMessages(SHOW_PRESS); 317 mHandler.removeMessages(LONG_PRESS); 318 } 319 if (distance > mLargeTouchSlopSquare) 320 { 321 mAlwaysInBiggerTapRegion = false; 322 } 323 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 324 } 325 else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) 326 { 327 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); 328 mLastMotionX = x; 329 mLastMotionY = y; 330 } 331 break; 332 333 case MotionEvent.ACTION_UP: 334 mStillDown = false; 335 MotionEvent currentUpEvent = MotionEvent.obtain(ev); 336 if (mIsDoubleTapping) 337 { 338 // Finally, give the up event of the double-tap 339 handled |= mDoubleTapListener.onDoubleTapEvent(ev); 340 } 341 else if (mInLongPress) 342 { 343 mHandler.removeMessages(TAP); 344 mListener.onLongPressUp(ev); 345 mInLongPress = false; 346 } 347 else if (mAlwaysInTapRegion) 348 { 349 handled = mListener.onSingleTapUp(mCurrentDownEvent); 350 } 351 else 352 { 353 // A fling must travel the minimum tap distance 354 } 355 if (mPreviousUpEvent != null) 356 { 357 mPreviousUpEvent.recycle(); 358 } 359 // Hold the event we obtained above - listeners may have changed the original. 360 mPreviousUpEvent = currentUpEvent; 361 mIsDoubleTapping = false; 362 mHandler.removeMessages(SHOW_PRESS); 363 mHandler.removeMessages(LONG_PRESS); 364 handled |= mListener.onUp(ev); 365 break; 366 case MotionEvent.ACTION_CANCEL: 367 cancel(); 368 break; 369 } 370 return handled; 371 } 372 cancel()373 private void cancel() 374 { 375 mHandler.removeMessages(SHOW_PRESS); 376 mHandler.removeMessages(LONG_PRESS); 377 mHandler.removeMessages(TAP); 378 mAlwaysInTapRegion = false; // ensures that we won't receive an OnSingleTap notification 379 // when a 2-Finger tap is performed 380 mIsDoubleTapping = false; 381 mStillDown = false; 382 if (mInLongPress) 383 { 384 mInLongPress = false; 385 } 386 } 387 isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, MotionEvent secondDown)388 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, 389 MotionEvent secondDown) 390 { 391 if (!mAlwaysInBiggerTapRegion) 392 { 393 return false; 394 } 395 396 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) 397 { 398 return false; 399 } 400 401 int deltaX = (int)firstDown.getX() - (int)secondDown.getX(); 402 int deltaY = (int)firstDown.getY() - (int)secondDown.getY(); 403 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); 404 } 405 dispatchLongPress()406 private void dispatchLongPress() 407 { 408 mHandler.removeMessages(TAP); 409 mInLongPress = true; 410 mListener.onLongPress(mCurrentDownEvent); 411 } 412 413 /** 414 * The listener that is used to notify when gestures occur. 415 * If you want to listen for all the different gestures then implement 416 * this interface. If you only want to listen for a subset it might 417 * be easier to extend {@link SimpleOnGestureListener}. 418 */ 419 public interface OnGestureListener { 420 421 /** 422 * Notified when a tap occurs with the down {@link MotionEvent} 423 * that triggered it. This will be triggered immediately for 424 * every down event. All other events should be preceded by this. 425 * 426 * @param e The down motion event. 427 */ onDown(MotionEvent e)428 boolean onDown(MotionEvent e); 429 430 /** 431 * Notified when a tap finishes with the up {@link MotionEvent} 432 * that triggered it. This will be triggered immediately for 433 * every up event. All other events should be preceded by this. 434 * 435 * @param e The up motion event. 436 */ onUp(MotionEvent e)437 boolean onUp(MotionEvent e); 438 439 /** 440 * The user has performed a down {@link MotionEvent} and not performed 441 * a move or up yet. This event is commonly used to provide visual 442 * feedback to the user to let them know that their action has been 443 * recognized i.e. highlight an element. 444 * 445 * @param e The down motion event 446 */ onShowPress(MotionEvent e)447 void onShowPress(MotionEvent e); 448 449 /** 450 * Notified when a tap occurs with the up {@link MotionEvent} 451 * that triggered it. 452 * 453 * @param e The up motion event that completed the first tap 454 * @return true if the event is consumed, else false 455 */ onSingleTapUp(MotionEvent e)456 boolean onSingleTapUp(MotionEvent e); 457 458 /** 459 * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the 460 * current move {@link MotionEvent}. The distance in x and y is also supplied for 461 * convenience. 462 * 463 * @param e1 The first down motion event that started the scrolling. 464 * @param e2 The move motion event that triggered the current onScroll. 465 * @param distanceX The distance along the X axis that has been scrolled since the last 466 * call to onScroll. This is NOT the distance between {@code e1} 467 * and {@code e2}. 468 * @param distanceY The distance along the Y axis that has been scrolled since the last 469 * call to onScroll. This is NOT the distance between {@code e1} 470 * and {@code e2}. 471 * @return true if the event is consumed, else false 472 */ onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)473 boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY); 474 475 /** 476 * Notified when a long press occurs with the initial on down {@link MotionEvent} 477 * that trigged it. 478 * 479 * @param e The initial on down motion event that started the longpress. 480 */ onLongPress(MotionEvent e)481 void onLongPress(MotionEvent e); 482 483 /** 484 * Notified when a long press ends with the final {@link MotionEvent}. 485 * 486 * @param e The up motion event that ended the longpress. 487 */ onLongPressUp(MotionEvent e)488 void onLongPressUp(MotionEvent e); 489 } 490 491 /** 492 * The listener that is used to notify when a double-tap or a confirmed 493 * single-tap occur. 494 */ 495 public interface OnDoubleTapListener { 496 /** 497 * Notified when a single-tap occurs. 498 * <p> 499 * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this 500 * will only be called after the detector is confident that the user's 501 * first tap is not followed by a second tap leading to a double-tap 502 * gesture. 503 * 504 * @param e The down motion event of the single-tap. 505 * @return true if the event is consumed, else false 506 */ onSingleTapConfirmed(MotionEvent e)507 boolean onSingleTapConfirmed(MotionEvent e); 508 509 /** 510 * Notified when a double-tap occurs. 511 * 512 * @param e The down motion event of the first tap of the double-tap. 513 * @return true if the event is consumed, else false 514 */ onDoubleTap(MotionEvent e)515 boolean onDoubleTap(MotionEvent e); 516 517 /** 518 * Notified when an event within a double-tap gesture occurs, including 519 * the down, move, and up events. 520 * 521 * @param e The motion event that occurred during the double-tap gesture. 522 * @return true if the event is consumed, else false 523 */ onDoubleTapEvent(MotionEvent e)524 boolean onDoubleTapEvent(MotionEvent e); 525 } 526 527 /** 528 * A convenience class to extend when you only want to listen for a subset 529 * of all the gestures. This implements all methods in the 530 * {@link OnGestureListener} and {@link OnDoubleTapListener} but does 531 * nothing and return {@code false} for all applicable methods. 532 */ 533 public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener 534 { onSingleTapUp(MotionEvent e)535 public boolean onSingleTapUp(MotionEvent e) 536 { 537 return false; 538 } 539 onLongPress(MotionEvent e)540 public void onLongPress(MotionEvent e) 541 { 542 } 543 onLongPressUp(MotionEvent e)544 public void onLongPressUp(MotionEvent e) 545 { 546 } 547 onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)548 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 549 { 550 return false; 551 } 552 onShowPress(MotionEvent e)553 public void onShowPress(MotionEvent e) 554 { 555 } 556 onDown(MotionEvent e)557 public boolean onDown(MotionEvent e) 558 { 559 return false; 560 } 561 onUp(MotionEvent e)562 public boolean onUp(MotionEvent e) 563 { 564 return false; 565 } 566 onDoubleTap(MotionEvent e)567 public boolean onDoubleTap(MotionEvent e) 568 { 569 return false; 570 } 571 onDoubleTapEvent(MotionEvent e)572 public boolean onDoubleTapEvent(MotionEvent e) 573 { 574 return false; 575 } 576 onSingleTapConfirmed(MotionEvent e)577 public boolean onSingleTapConfirmed(MotionEvent e) 578 { 579 return false; 580 } 581 } 582 583 private class GestureHandler extends Handler 584 { GestureHandler()585 GestureHandler() 586 { 587 super(); 588 } 589 GestureHandler(Handler handler)590 GestureHandler(Handler handler) 591 { 592 super(handler.getLooper()); 593 } 594 handleMessage(Message msg)595 @Override public void handleMessage(Message msg) 596 { 597 switch (msg.what) 598 { 599 case SHOW_PRESS: 600 mListener.onShowPress(mCurrentDownEvent); 601 break; 602 603 case LONG_PRESS: 604 dispatchLongPress(); 605 break; 606 607 case TAP: 608 // If the user's finger is still down, do not count it as a tap 609 if (mDoubleTapListener != null && !mStillDown) 610 { 611 mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); 612 } 613 break; 614 615 default: 616 throw new RuntimeException("Unknown message " + msg); // never 617 } 618 } 619 } 620 } 621