/ime/latinime/src/com/googlecode/eyesfree/inputmethod/MultitouchGestureDetector.java

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