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