/ime/latinime/src/com/googlecode/eyesfree/inputmethod/MultitouchGestureDetector.java
Java | 526 lines | 316 code | 83 blank | 127 comment | 73 complexity | 6d6a03e3906de20097feff4624eaef8f MD5 | raw file
1/* 2 * Copyright (C) 2011 Google Inc. 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 com.googlecode.eyesfree.inputmethod; 18 19import android.content.Context; 20import android.os.Handler; 21import android.os.Message; 22import android.view.MotionEvent; 23import android.view.VelocityTracker; 24import android.view.View; 25import android.view.ViewConfiguration; 26import android.view.accessibility.AccessibilityManager; 27 28import com.googlecode.eyesfree.utils.compat.AccessibilityManagerCompatUtils; 29import com.googlecode.eyesfree.utils.compat.InputDeviceCompatUtils; 30import com.googlecode.eyesfree.utils.compat.MotionEventCompatUtils; 31 32/** 33 * Detects various gestures and events using the supplied {@link MotionEvent}s. 34 * The {@link MultitouchGestureListener} callback will notify users when a 35 * particular motion event has occurred. This class should only be used with 36 * {@link MotionEvent}s reported via touch (don't use for trackball events). To 37 * use this class: 38 * <ul> 39 * <li>Create an instance of the {@code GestureDetector} for your {@link View} 40 * <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call 41 * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback will 42 * be executed when the events occur. 43 * </ul> 44 */ 45public class MultitouchGestureDetector { 46 public interface MultitouchGestureListener { 47 public boolean onDown(MotionEvent ev); 48 49 public boolean onTap(MotionEvent ev); 50 51 public boolean onDoubleTap(MotionEvent ev); 52 53 public boolean onLongPress(MotionEvent ev); 54 55 public boolean onSlideTap(MotionEvent ev); 56 57 public boolean onMove(MotionEvent ev); 58 59 public boolean onFlick(MotionEvent e1, MotionEvent e2); 60 } 61 62 private int mTouchSlopSquare; 63 private int mDoubleTapSlopSquare; 64 65 private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); 66 private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); 67 private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); 68 69 /** The maximum duration of a flick gesture in milliseconds. */ 70 private static final int FLICK_TIMEOUT = 250; 71 72 // constants for Message.what used by GestureHandler below 73 private static final int LONG_PRESS = 1; 74 private static final int FIRST_TAP = 2; 75 private static final int SLIDE_TAP = 3; 76 private static final int FLICK = 4; 77 private static final int SHOW_PRESS = 5; 78 79 private final Handler mHandler; 80 private final AccessibilityManager mAccessibilityManager; 81 82 private MultitouchGestureListener mListener; 83 84 private boolean mInLongPress; 85 private boolean mAlwaysInTapRegion; 86 private boolean mAlwaysInDoubleTapRegion; 87 88 private MotionEvent mCurrentDownEvent; 89 private MotionEvent mPreviousUpEvent; 90 91 /** 92 * The first UP (or POINTER_UP) event to occur after a DOWN (or 93 * POINTER_DOWN) event. This gets reset to null on DOWN events. 94 */ 95 private MotionEvent mFirstUpEvent; 96 97 /** 98 * True when the user is still touching for the second tap (down, move, and 99 * up events). Can only be true if there is a double tap listener attached. 100 */ 101 private boolean mIsDoubleTapping; 102 103 /** Whether the user is still holding a finger on the screen. */ 104 private boolean mIsStillDown; 105 106 /** The event time from the last received event. */ 107 private long mLastEventTime; 108 109 private boolean mIsLongPressEnabled; 110 private boolean mIsDoubleTapEnabled; 111 private int mDoubleTapMinFingers; 112 113 /** 114 * Determines speed during touch scrolling 115 */ 116 private VelocityTracker mVelocityTracker; 117 118 private class MultitouchGestureHandler extends Handler { 119 MultitouchGestureHandler() { 120 super(); 121 } 122 123 MultitouchGestureHandler(Handler handler) { 124 super(handler.getLooper()); 125 } 126 127 @Override 128 public void handleMessage(Message msg) { 129 switch (msg.what) { 130 case SHOW_PRESS: 131 mListener.onDown(mCurrentDownEvent); 132 break; 133 case FIRST_TAP: 134 if (!mIsStillDown) { 135 mListener.onTap(mCurrentDownEvent); 136 } 137 break; 138 case LONG_PRESS: 139 mHandler.removeMessages(FIRST_TAP); 140 mInLongPress = true; 141 mListener.onLongPress(mCurrentDownEvent); 142 break; 143 case FLICK: 144 break; 145 } 146 } 147 } 148 149 /** 150 * Creates a GestureDetector with no listener. You may only use this 151 * constructor from a UI thread (this is the usual situation). 152 * 153 * @see android.os.Handler#Handler() 154 * @param context the application's context 155 */ 156 public MultitouchGestureDetector(Context context) { 157 this(context, null, null); 158 } 159 160 /** 161 * Creates a GestureDetector with the supplied listener. You may only use 162 * this constructor from a UI thread (this is the usual situation). 163 * 164 * @see android.os.Handler#Handler() 165 * @param context the application's context 166 * @param listener the listener invoked for all the callbacks 167 */ 168 public MultitouchGestureDetector(Context context, MultitouchGestureListener listener) { 169 this(context, listener, null); 170 } 171 172 /** 173 * Creates a GestureDetector with the supplied listener. You may only use 174 * this constructor from a UI thread (this is the usual situation). 175 * 176 * @see android.os.Handler#Handler() 177 * @param context the application's context 178 * @param listener the listener invoked for all the callbacks 179 * @param handler the handler to use 180 */ 181 public MultitouchGestureDetector(Context context, MultitouchGestureListener listener, 182 Handler handler) { 183 if (handler != null) { 184 mHandler = new MultitouchGestureHandler(handler); 185 } else { 186 mHandler = new MultitouchGestureHandler(); 187 } 188 189 mAccessibilityManager = 190 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 191 mListener = listener; 192 193 init(context); 194 } 195 196 /** 197 * Sets the listener. 198 * 199 * @param listener The listener invoked for all the callbacks. 200 */ 201 public void setListener(MultitouchGestureListener listener) { 202 mListener = listener; 203 } 204 205 private void init(Context context) { 206 mIsLongPressEnabled = true; 207 mIsDoubleTapEnabled = true; 208 mDoubleTapMinFingers = 1; 209 210 final ViewConfiguration configuration = ViewConfiguration.get(context); 211 final int touchSlop = (int) (0.5 * configuration.getScaledTouchSlop()); 212 final int doubleTapSlop = configuration.getScaledDoubleTapSlop(); 213 214 mTouchSlopSquare = touchSlop * touchSlop; 215 mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; 216 } 217 218 /** 219 * Sets whether double tap is enabled. If this is enabled, when a user 220 * quickly taps twice you get a double tap event and nothing further. If 221 * it's disabled, you get two tap events. By default, double tap is enabled. 222 * 223 * @param isDoubleTapEnabled Whether double tap should be enabled. 224 */ 225 public void setIsDoubleTapEnabled(boolean isDoubleTapEnabled) { 226 mIsDoubleTapEnabled = isDoubleTapEnabled; 227 } 228 229 /** 230 * Sets the minimum number of fingers required for a double-tap event. By 231 * default, this number is one. 232 * 233 * @param count The minimum number of fingers required for a double-tap. 234 * @see #setIsDoubleTapEnabled(boolean) 235 */ 236 public void setDoubleTapMinFingers(int count) { 237 mDoubleTapMinFingers = count; 238 } 239 240 /** 241 * Sets whether long press is enabled. If this is enabled, when a user 242 * presses and holds down you get a long press event and nothing further. If 243 * it's disabled, the user can press and hold down and then later move their 244 * finger and you will get scroll events. By default, long press is enabled. 245 * 246 * @param isLongPressEnabled Whether long press should be enabled. 247 */ 248 public void setIsLongPressEnabled(boolean isLongPressEnabled) { 249 mIsLongPressEnabled = isLongPressEnabled; 250 } 251 252 /** 253 * @return true if long press is enabled, else false. 254 */ 255 public boolean isLongPressEnabled() { 256 return mIsLongPressEnabled; 257 } 258 259 /** 260 * Analyzes the given motion event and if applicable triggers the 261 * appropriate callbacks on the {@link MultitouchGestureListener} supplied. 262 * 263 * @param ev The current motion event. 264 * @return true if the {@link MultitouchGestureListener} consumed the event, 265 * else false. 266 */ 267 public boolean onTouchEvent(MotionEvent ev) { 268 if (mListener == null) { 269 return false; 270 } 271 272 // If touch exploration is enabled, use the following workarounds: 273 // 1. Detect two-finger scroll and adjust the pointer count. 274 // 2. Detect hover to touch transition and drop extra up event. 275 if (AccessibilityManagerCompatUtils.isTouchExplorationEnabled(mAccessibilityManager)) { 276 handleTouchExploration(ev); 277 } 278 279 mLastEventTime = ev.getEventTime(); 280 281 final int action = ev.getAction(); 282 final float y = ev.getY(); 283 final float x = ev.getX(); 284 285 if (mVelocityTracker == null) { 286 mVelocityTracker = VelocityTracker.obtain(); 287 } 288 mVelocityTracker.addMovement(ev); 289 290 boolean handled = false; 291 292 switch (action & MotionEvent.ACTION_MASK) { 293 case MotionEvent.ACTION_DOWN: 294 if (mIsDoubleTapEnabled) { 295 boolean withinFirstTapTimeout = mHandler.hasMessages(FIRST_TAP); 296 mHandler.removeMessages(FIRST_TAP); 297 298 if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) 299 && withinFirstTapTimeout 300 && isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { 301 mIsDoubleTapping = true; 302 mHandler.removeMessages(SHOW_PRESS); 303 } else { 304 mHandler.sendEmptyMessageAtTime(FIRST_TAP, ev.getEventTime() 305 + DOUBLE_TAP_TIMEOUT); 306 } 307 } 308 309 setCurrentDownEvent(ev); 310 setFirstUpEvent(null); 311 312 mAlwaysInTapRegion = true; 313 mAlwaysInDoubleTapRegion = true; 314 mInLongPress = false; 315 mIsStillDown = true; 316 317 if (mIsLongPressEnabled) { 318 mHandler.removeMessages(LONG_PRESS); 319 mHandler.sendEmptyMessageAtTime(LONG_PRESS, ev.getDownTime() + TAP_TIMEOUT 320 + LONGPRESS_TIMEOUT); 321 } 322 323 mHandler.sendEmptyMessageAtTime(FLICK, ev.getDownTime() + FLICK_TIMEOUT); 324 mHandler.sendEmptyMessageAtTime(SHOW_PRESS, ev.getDownTime() + TAP_TIMEOUT); 325 326 handled = true; 327 328 break; 329 case MotionEvent.ACTION_POINTER_DOWN: 330 setCurrentDownEvent(ev); 331 setFirstUpEvent(null); 332 333 // If this is a second finger, start the SLIDE_PRESS timeout. 334 if (!mAlwaysInTapRegion && ev.getPointerCount() == 2) { 335 mHandler.sendEmptyMessageAtTime(SLIDE_TAP, ev.getEventTime() + TAP_TIMEOUT); 336 } 337 338 handled = true; 339 340 break; 341 case MotionEvent.ACTION_MOVE: 342 if (mAlwaysInTapRegion) { 343 // We might still be in the tap region, so check pointer 344 // distances. 345 int distance = maxPointerDistanceSquared(ev, mCurrentDownEvent); 346 if (distance > mTouchSlopSquare) { 347 mAlwaysInTapRegion = false; 348 mHandler.removeMessages(SLIDE_TAP); 349 mHandler.removeMessages(FIRST_TAP); 350 mHandler.removeMessages(LONG_PRESS); 351 mHandler.removeMessages(SHOW_PRESS); 352 } 353 if (distance > mDoubleTapSlopSquare) { 354 mAlwaysInDoubleTapRegion = false; 355 } 356 handled = true; 357 } else if (mHandler.hasMessages(FLICK)) { 358 handled = true; 359 } else { 360 // We're outside the tap region and there are no other 361 // options. 362 handled |= mListener.onMove(ev); 363 } 364 break; 365 case MotionEvent.ACTION_POINTER_UP: 366 if (mFirstUpEvent == null) { 367 setFirstUpEvent(ev); 368 } 369 370 // If this is a second finger, check for the SLIDE_PRESS 371 // timeout. 372 if (!mAlwaysInTapRegion && ev.getPointerCount() == 2 373 && mHandler.hasMessages(SLIDE_TAP)) { 374 setCurrentDownEvent(ev); 375 handled |= mListener.onSlideTap(ev); 376 } else { 377 handled = true; 378 } 379 380 mHandler.removeMessages(SLIDE_TAP); 381 382 break; 383 case MotionEvent.ACTION_UP: 384 if (mFirstUpEvent == null) { 385 setFirstUpEvent(ev); 386 } 387 388 if (mPreviousUpEvent != null) { 389 mPreviousUpEvent.recycle(); 390 } 391 mPreviousUpEvent = MotionEvent.obtain(ev); 392 393 // TODO(alanv): This could be replaced with a check for 394 // hasMessage(LONG_PRESS) 395 mIsStillDown = false; 396 397 if (mIsDoubleTapping) { 398 handled |= mListener.onDoubleTap(mCurrentDownEvent); 399 mIsDoubleTapping = false; 400 } else if (mInLongPress) { 401 mHandler.removeMessages(FIRST_TAP); 402 mInLongPress = false; 403 } else if (!mIsDoubleTapEnabled && mAlwaysInTapRegion) { 404 handled |= mListener.onTap(mCurrentDownEvent); 405 } else if (!mAlwaysInTapRegion && mHandler.hasMessages(FLICK)) { 406 mHandler.removeMessages(FIRST_TAP); 407 handled |= mListener.onFlick(mCurrentDownEvent, mFirstUpEvent); 408 } else if (mHandler.hasMessages(FIRST_TAP)) { 409 // If we don't have enough fingers down for a double-tap 410 // event, just go ahead and send the tap event. 411 if (mCurrentDownEvent.getPointerCount() < mDoubleTapMinFingers) { 412 mHandler.removeMessages(FIRST_TAP); 413 mListener.onTap(mCurrentDownEvent); 414 } 415 handled = true; 416 } 417 418 mHandler.removeMessages(FLICK); 419 mHandler.removeMessages(LONG_PRESS); 420 mHandler.removeMessages(SHOW_PRESS); 421 break; 422 case MotionEvent.ACTION_CANCEL: 423 cancel(); 424 break; 425 case MotionEvent.ACTION_OUTSIDE: 426 // Consume and ignore this action. 427 handled = true; 428 break; 429 } 430 431 return handled; 432 } 433 434 private void handleTouchExploration(MotionEvent ev) { 435 // Cancel duplicate events, these are touch exploration bugs. 436 if (ev.getEventTime() == mLastEventTime) { 437 ev.setAction(MotionEvent.ACTION_OUTSIDE); 438 return; 439 } 440 441 // Adjust the pointer count on single-touch events. 442 if ((ev.getPointerCount() == 1) 443 && (MotionEventCompatUtils.getSource(ev) == InputDeviceCompatUtils.SOURCE_TOUCHSCREEN)) { 444 SimpleMultitouchGestureListener.setFakePointerCount(ev, 2); 445 } 446 } 447 448 private void setFirstUpEvent(MotionEvent ev) { 449 if (mFirstUpEvent != null) { 450 mFirstUpEvent.recycle(); 451 } 452 453 if (ev != null) { 454 mFirstUpEvent = MotionEvent.obtain(ev); 455 } else { 456 mFirstUpEvent = null; 457 } 458 } 459 460 private void setCurrentDownEvent(MotionEvent ev) { 461 if (mCurrentDownEvent != null) { 462 mCurrentDownEvent.recycle(); 463 } 464 465 mCurrentDownEvent = MotionEvent.obtain(ev); 466 } 467 468 private int maxPointerDistanceSquared(MotionEvent e1, MotionEvent e2) { 469 // Ensure that the events have the same number of pointers. 470 if (e1.getPointerCount() != e2.getPointerCount()) { 471 return Integer.MAX_VALUE; 472 } 473 474 int maxDistance = 0; 475 int pointerCount = e1.getPointerCount(); 476 477 for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { 478 // Ensure that the events have the same pointer index mapping. 479 if (e1.getPointerId(pointerIndex) != e2.getPointerId(pointerIndex)) { 480 return Integer.MAX_VALUE; 481 } 482 483 int deltaX = (int) (e1.getX(pointerIndex) - e2.getX(pointerIndex)); 484 int deltaY = (int) (e1.getY(pointerIndex) - e2.getY(pointerIndex)); 485 int distance = (deltaX * deltaX + deltaY * deltaY); 486 487 if (distance > maxDistance) { 488 maxDistance = distance; 489 } 490 } 491 492 return maxDistance; 493 } 494 495 private void cancel() { 496 // Remove all pending messages. 497 mHandler.removeMessages(SHOW_PRESS); 498 mHandler.removeMessages(FIRST_TAP); 499 mHandler.removeMessages(LONG_PRESS); 500 mHandler.removeMessages(FLICK); 501 502 // Clear the velocity tracker. 503 mVelocityTracker.recycle(); 504 mVelocityTracker = null; 505 506 // Clear the current state. 507 mIsDoubleTapping = false; 508 mInLongPress = false; 509 mIsStillDown = false; 510 } 511 512 private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, 513 MotionEvent secondDown) { 514 if (!mAlwaysInDoubleTapRegion) { 515 return false; 516 } 517 518 if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { 519 return false; 520 } 521 522 int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); 523 int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); 524 return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); 525 } 526}