/ime/latinime/src/com/googlecode/eyesfree/inputmethod/latin/PointerTracker.java

http://eyes-free.googlecode.com/ · Java · 546 lines · 425 code · 70 blank · 51 comment · 105 complexity · 4821467f9a885eb8575ad1477ca9d99c MD5 · raw file

  1. /*
  2. * Copyright (C) 2010 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * 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, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.googlecode.eyesfree.inputmethod.latin;
  17. import android.content.res.Resources;
  18. import android.inputmethodservice.Keyboard;
  19. import android.inputmethodservice.Keyboard.Key;
  20. import android.util.Log;
  21. import android.view.MotionEvent;
  22. import com.googlecode.eyesfree.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener;
  23. import com.googlecode.eyesfree.inputmethod.latin.LatinKeyboardBaseView.UIHandler;
  24. public class PointerTracker {
  25. private static final String TAG = "PointerTracker";
  26. private static final boolean DEBUG = false;
  27. private static final boolean DEBUG_MOVE = false;
  28. public interface UIProxy {
  29. public void invalidateKey(Key key);
  30. public void showPreview(int keyIndex, PointerTracker tracker);
  31. public boolean hasDistinctMultitouch();
  32. public boolean isAccessibilityEnabled();
  33. }
  34. public final int mPointerId;
  35. // Timing constants
  36. private final int mDelayBeforeKeyRepeatStart;
  37. private final int mLongPressKeyTimeout;
  38. private final int mMultiTapKeyTimeout;
  39. // Miscellaneous constants
  40. private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY;
  41. private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
  42. private final UIProxy mProxy;
  43. private final UIHandler mHandler;
  44. private final KeyDetector mKeyDetector;
  45. private OnKeyboardActionListener mListener;
  46. private final boolean mHasDistinctMultitouch;
  47. private boolean mIsAccessibilityEnabled;
  48. private Key[] mKeys;
  49. private int mKeyHysteresisDistanceSquared = -1;
  50. private final KeyState mKeyState;
  51. // true if event is already translated to a key action (long press or mini-keyboard)
  52. private boolean mKeyAlreadyProcessed;
  53. // true if this pointer is repeatable key
  54. private boolean mIsRepeatableKey;
  55. // For multi-tap
  56. private int mLastSentIndex;
  57. private int mTapCount;
  58. private long mLastTapTime;
  59. private boolean mInMultiTap;
  60. private final StringBuilder mPreviewLabel = new StringBuilder(1);
  61. // pressed key
  62. private int mPreviousKey = NOT_A_KEY;
  63. // This class keeps track of a key index and a position where this pointer is.
  64. private static class KeyState {
  65. private final KeyDetector mKeyDetector;
  66. // The position and time at which first down event occurred.
  67. private int mStartX;
  68. private int mStartY;
  69. private long mDownTime;
  70. // The current key index where this pointer is.
  71. private int mKeyIndex = NOT_A_KEY;
  72. // The position where mKeyIndex was recognized for the first time.
  73. private int mKeyX;
  74. private int mKeyY;
  75. // Last pointer position.
  76. private int mLastX;
  77. private int mLastY;
  78. public KeyState(KeyDetector keyDetecor) {
  79. mKeyDetector = keyDetecor;
  80. }
  81. public int getKeyIndex() {
  82. return mKeyIndex;
  83. }
  84. public int getKeyX() {
  85. return mKeyX;
  86. }
  87. public int getKeyY() {
  88. return mKeyY;
  89. }
  90. public int getStartX() {
  91. return mStartX;
  92. }
  93. public int getStartY() {
  94. return mStartY;
  95. }
  96. public long getDownTime() {
  97. return mDownTime;
  98. }
  99. public int getLastX() {
  100. return mLastX;
  101. }
  102. public int getLastY() {
  103. return mLastY;
  104. }
  105. public int onDownKey(int x, int y, long eventTime) {
  106. mStartX = x;
  107. mStartY = y;
  108. mDownTime = eventTime;
  109. return onMoveToNewKey(onMoveKeyInternal(x, y), x, y);
  110. }
  111. private int onMoveKeyInternal(int x, int y) {
  112. mLastX = x;
  113. mLastY = y;
  114. return mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
  115. }
  116. public int onMoveKey(int x, int y) {
  117. return onMoveKeyInternal(x, y);
  118. }
  119. public int onMoveToNewKey(int keyIndex, int x, int y) {
  120. mKeyIndex = keyIndex;
  121. mKeyX = x;
  122. mKeyY = y;
  123. return keyIndex;
  124. }
  125. public int onUpKey(int x, int y) {
  126. return onMoveKeyInternal(x, y);
  127. }
  128. public void onSetKeyboard() {
  129. mKeyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(mKeyX, mKeyY, null);
  130. }
  131. }
  132. public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy,
  133. Resources res) {
  134. if (proxy == null || handler == null || keyDetector == null)
  135. throw new NullPointerException();
  136. mPointerId = id;
  137. mProxy = proxy;
  138. mHandler = handler;
  139. mKeyDetector = keyDetector;
  140. mKeyState = new KeyState(keyDetector);
  141. mHasDistinctMultitouch = proxy.hasDistinctMultitouch();
  142. mIsAccessibilityEnabled = proxy.isAccessibilityEnabled();
  143. mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start);
  144. mLongPressKeyTimeout = res.getInteger(R.integer.config_long_press_key_timeout);
  145. mMultiTapKeyTimeout = res.getInteger(R.integer.config_multi_tap_key_timeout);
  146. resetMultiTap();
  147. }
  148. public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
  149. mListener = listener;
  150. }
  151. public void setAccessibilityEnabled(boolean accessibilityEnabled) {
  152. mIsAccessibilityEnabled = accessibilityEnabled;
  153. }
  154. public void setKeyboard(Key[] keys, float keyHysteresisDistance) {
  155. if (keys == null || keyHysteresisDistance < 0)
  156. throw new IllegalArgumentException();
  157. mKeys = keys;
  158. mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance);
  159. // Update current key index because keyboard layout has been changed.
  160. mKeyState.onSetKeyboard();
  161. }
  162. private boolean isValidKeyIndex(int keyIndex) {
  163. return keyIndex >= 0 && keyIndex < mKeys.length;
  164. }
  165. public Key getKey(int keyIndex) {
  166. return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
  167. }
  168. private boolean isModifierInternal(int keyIndex) {
  169. Key key = getKey(keyIndex);
  170. if (key == null)
  171. return false;
  172. int primaryCode = key.codes[0];
  173. return primaryCode == Keyboard.KEYCODE_SHIFT
  174. || primaryCode == Keyboard.KEYCODE_MODE_CHANGE;
  175. }
  176. public boolean isModifier() {
  177. return isModifierInternal(mKeyState.getKeyIndex());
  178. }
  179. public boolean isOnModifierKey(int x, int y) {
  180. return isModifierInternal(mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null));
  181. }
  182. public boolean isSpaceKey(int keyIndex) {
  183. Key key = getKey(keyIndex);
  184. return key != null && key.codes[0] == LatinIME.KEYCODE_SPACE;
  185. }
  186. public void updateKey(int keyIndex) {
  187. if (mKeyAlreadyProcessed)
  188. return;
  189. int oldKeyIndex = mPreviousKey;
  190. mPreviousKey = keyIndex;
  191. if (keyIndex != oldKeyIndex) {
  192. if (isValidKeyIndex(oldKeyIndex)) {
  193. // if new key index is not a key, old key was just released inside of the key.
  194. final boolean inside = (keyIndex == NOT_A_KEY);
  195. mKeys[oldKeyIndex].onReleased(inside);
  196. mProxy.invalidateKey(mKeys[oldKeyIndex]);
  197. }
  198. if (isValidKeyIndex(keyIndex)) {
  199. mKeys[keyIndex].onPressed();
  200. mProxy.invalidateKey(mKeys[keyIndex]);
  201. }
  202. }
  203. }
  204. public void setAlreadyProcessed() {
  205. mKeyAlreadyProcessed = true;
  206. }
  207. public void onTouchEvent(int action, int x, int y, long eventTime) {
  208. switch (action) {
  209. case MotionEvent.ACTION_MOVE:
  210. onMoveEvent(x, y, eventTime);
  211. break;
  212. case MotionEvent.ACTION_DOWN:
  213. case MotionEvent.ACTION_POINTER_DOWN:
  214. onDownEvent(x, y, eventTime);
  215. break;
  216. case MotionEvent.ACTION_UP:
  217. case MotionEvent.ACTION_POINTER_UP:
  218. onUpEvent(x, y, eventTime);
  219. break;
  220. case MotionEvent.ACTION_CANCEL:
  221. onCancelEvent(x, y, eventTime);
  222. break;
  223. }
  224. }
  225. public void onDownEvent(int x, int y, long eventTime) {
  226. if (DEBUG)
  227. debugLog("onDownEvent:", x, y);
  228. int keyIndex = mKeyState.onDownKey(x, y, eventTime);
  229. mKeyAlreadyProcessed = false;
  230. mIsRepeatableKey = false;
  231. checkMultiTap(eventTime, keyIndex);
  232. if (mListener != null) {
  233. if (isValidKeyIndex(keyIndex)) {
  234. mListener.onPress(mKeys[keyIndex]);
  235. // This onPress call may have changed keyboard layout and have updated mKeyIndex.
  236. // If that's the case, mKeyIndex has been updated in setKeyboard().
  237. keyIndex = mKeyState.getKeyIndex();
  238. }
  239. }
  240. if (isValidKeyIndex(keyIndex)) {
  241. // Accessibility disables key repeat because users may need to pause on a key to hear
  242. // its spoken description.
  243. if (mKeys[keyIndex].repeatable && !mIsAccessibilityEnabled) {
  244. repeatKey(keyIndex);
  245. mHandler.startKeyRepeatTimer(mDelayBeforeKeyRepeatStart, keyIndex, this);
  246. mIsRepeatableKey = true;
  247. }
  248. mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
  249. }
  250. showKeyPreviewAndUpdateKey(keyIndex);
  251. }
  252. public void onMoveEvent(int x, int y, long eventTime) {
  253. if (DEBUG_MOVE)
  254. debugLog("onMoveEvent:", x, y);
  255. if (mKeyAlreadyProcessed)
  256. return;
  257. KeyState keyState = mKeyState;
  258. final int keyIndex = keyState.onMoveKey(x, y);
  259. final Key oldKey = getKey(keyState.getKeyIndex());
  260. if (isValidKeyIndex(keyIndex)) {
  261. if (oldKey == null) {
  262. if (mListener != null)
  263. mListener.onPress(mKeys[keyIndex]);
  264. keyState.onMoveToNewKey(keyIndex, x, y);
  265. mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
  266. } else if (!isMinorMoveBounce(x, y, keyIndex)) {
  267. if (mListener != null) {
  268. mListener.onLeaving(oldKey);
  269. mListener.onPress(mKeys[keyIndex]);
  270. }
  271. resetMultiTap();
  272. keyState.onMoveToNewKey(keyIndex, x, y);
  273. mHandler.startLongPressTimer(mLongPressKeyTimeout, keyIndex, this);
  274. }
  275. } else {
  276. if (oldKey != null) {
  277. if (mListener != null)
  278. mListener.onLeaving(oldKey);
  279. keyState.onMoveToNewKey(keyIndex, x ,y);
  280. mHandler.cancelLongPressTimer();
  281. } else if (!isMinorMoveBounce(x, y, keyIndex)) {
  282. resetMultiTap();
  283. keyState.onMoveToNewKey(keyIndex, x ,y);
  284. mHandler.cancelLongPressTimer();
  285. }
  286. }
  287. showKeyPreviewAndUpdateKey(mKeyState.getKeyIndex());
  288. }
  289. public void onUpEvent(int x, int y, long eventTime) {
  290. if (DEBUG)
  291. debugLog("onUpEvent :", x, y);
  292. if (mKeyAlreadyProcessed)
  293. return;
  294. mHandler.cancelKeyTimers();
  295. mHandler.cancelPopupPreview();
  296. int keyIndex = mKeyState.onUpKey(x, y);
  297. if (isMinorMoveBounce(x, y, keyIndex)) {
  298. // Use previous fixed key index and coordinates.
  299. keyIndex = mKeyState.getKeyIndex();
  300. x = mKeyState.getKeyX();
  301. y = mKeyState.getKeyY();
  302. }
  303. showKeyPreviewAndUpdateKey(NOT_A_KEY);
  304. if (!mIsRepeatableKey) {
  305. detectAndSendKey(keyIndex, x, y, eventTime);
  306. }
  307. if (isValidKeyIndex(keyIndex))
  308. mProxy.invalidateKey(mKeys[keyIndex]);
  309. }
  310. public void onCancelEvent(int x, int y, long eventTime) {
  311. if (DEBUG)
  312. debugLog("onCancelEvt:", x, y);
  313. mHandler.cancelKeyTimers();
  314. mHandler.cancelPopupPreview();
  315. showKeyPreviewAndUpdateKey(NOT_A_KEY);
  316. int keyIndex = mKeyState.getKeyIndex();
  317. if (isValidKeyIndex(keyIndex))
  318. mProxy.invalidateKey(mKeys[keyIndex]);
  319. }
  320. public void repeatKey(int keyIndex) {
  321. Key key = getKey(keyIndex);
  322. if (key != null) {
  323. // While key is repeating, because there is no need to handle multi-tap key, we can
  324. // pass -1 as eventTime argument.
  325. detectAndSendKey(keyIndex, key.x, key.y, -1);
  326. }
  327. }
  328. public int getLastX() {
  329. return mKeyState.getLastX();
  330. }
  331. public int getLastY() {
  332. return mKeyState.getLastY();
  333. }
  334. public long getDownTime() {
  335. return mKeyState.getDownTime();
  336. }
  337. // These package scope methods are only for debugging purpose.
  338. /* package */ int getStartX() {
  339. return mKeyState.getStartX();
  340. }
  341. /* package */ int getStartY() {
  342. return mKeyState.getStartY();
  343. }
  344. private boolean isMinorMoveBounce(int x, int y, int newKey) {
  345. if (mKeys == null || mKeyHysteresisDistanceSquared < 0)
  346. throw new IllegalStateException("keyboard and/or hysteresis not set");
  347. int curKey = mKeyState.getKeyIndex();
  348. if (newKey == curKey) {
  349. return true;
  350. } else if (isValidKeyIndex(curKey)) {
  351. return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) < mKeyHysteresisDistanceSquared;
  352. } else {
  353. return false;
  354. }
  355. }
  356. private static int getSquareDistanceToKeyEdge(int x, int y, Key key) {
  357. final int left = key.x;
  358. final int right = key.x + key.width;
  359. final int top = key.y;
  360. final int bottom = key.y + key.height;
  361. final int edgeX = x < left ? left : (x > right ? right : x);
  362. final int edgeY = y < top ? top : (y > bottom ? bottom : y);
  363. final int dx = x - edgeX;
  364. final int dy = y - edgeY;
  365. return dx * dx + dy * dy;
  366. }
  367. private void showKeyPreviewAndUpdateKey(int keyIndex) {
  368. updateKey(keyIndex);
  369. // The modifier key, such as shift key, should not be shown as preview when multi-touch is
  370. // supported. On the other hand, if multi-touch is not supported, the modifier key should
  371. // be shown as preview. If accessibility is turned on, the modifier key should be shown as
  372. // preview.
  373. if (mHasDistinctMultitouch && isModifier() && !mIsAccessibilityEnabled) {
  374. mProxy.showPreview(NOT_A_KEY, this);
  375. } else {
  376. mProxy.showPreview(keyIndex, this);
  377. }
  378. }
  379. private void detectAndSendKey(int index, int x, int y, long eventTime) {
  380. final OnKeyboardActionListener listener = mListener;
  381. final Key key = getKey(index);
  382. if (key == null) {
  383. if (listener != null)
  384. listener.onCancel();
  385. } else {
  386. if (key.text != null) {
  387. if (listener != null) {
  388. listener.onText(key.text);
  389. listener.onRelease(null);
  390. }
  391. } else {
  392. int code = key.codes[0];
  393. //TextEntryState.keyPressedAt(key, x, y);
  394. int[] codes = mKeyDetector.newCodeArray();
  395. mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes);
  396. // Multi-tap
  397. if (mInMultiTap) {
  398. if (mTapCount != -1) {
  399. mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y);
  400. } else {
  401. mTapCount = 0;
  402. }
  403. code = key.codes[mTapCount];
  404. }
  405. /*
  406. * Swap the first and second values in the codes array if the primary code is not
  407. * the first value but the second value in the array. This happens when key
  408. * debouncing is in effect.
  409. */
  410. if (codes.length >= 2 && codes[0] != code && codes[1] == code) {
  411. codes[1] = codes[0];
  412. codes[0] = code;
  413. }
  414. if (listener != null) {
  415. listener.onKey(code, codes, x, y);
  416. listener.onRelease(key);
  417. }
  418. }
  419. mLastSentIndex = index;
  420. mLastTapTime = eventTime;
  421. }
  422. }
  423. /**
  424. * Handle multi-tap keys by producing the key label for the current multi-tap state.
  425. */
  426. public CharSequence getPreviewText(Key key) {
  427. if (mInMultiTap) {
  428. // Multi-tap
  429. mPreviewLabel.setLength(0);
  430. mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
  431. return mPreviewLabel;
  432. } else {
  433. return key.label;
  434. }
  435. }
  436. private void resetMultiTap() {
  437. mLastSentIndex = NOT_A_KEY;
  438. mTapCount = 0;
  439. mLastTapTime = -1;
  440. mInMultiTap = false;
  441. }
  442. private void checkMultiTap(long eventTime, int keyIndex) {
  443. Key key = getKey(keyIndex);
  444. if (key == null)
  445. return;
  446. final boolean isMultiTap =
  447. (eventTime < mLastTapTime + mMultiTapKeyTimeout && keyIndex == mLastSentIndex);
  448. if (key.codes.length > 1) {
  449. mInMultiTap = true;
  450. if (isMultiTap) {
  451. mTapCount = (mTapCount + 1) % key.codes.length;
  452. return;
  453. } else {
  454. mTapCount = -1;
  455. return;
  456. }
  457. }
  458. if (!isMultiTap) {
  459. resetMultiTap();
  460. }
  461. }
  462. private void debugLog(String title, int x, int y) {
  463. int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null);
  464. Key key = getKey(keyIndex);
  465. final String code;
  466. if (key == null) {
  467. code = "----";
  468. } else {
  469. int primaryCode = key.codes[0];
  470. code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode);
  471. }
  472. Log.d(TAG, String.format("%s%s[%d] %3d,%3d %3d(%s) %s", title,
  473. (mKeyAlreadyProcessed ? "-" : " "), mPointerId, x, y, keyIndex, code,
  474. (isModifier() ? "modifier" : "")));
  475. }
  476. }