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