diff options
author | 2010-09-03 22:57:28 +0900 | |
---|---|---|
committer | 2010-09-03 22:57:28 +0900 | |
commit | 4d67480fe297f3b9265fcbeb1d1651b910bd96c2 (patch) | |
tree | 56ec6666d6c13e1daefd43ed9fbe44988d42fb8c /java/src/com/android/inputmethod/latin/PointerTracker.java | |
parent | 17fe18350fe5b411f78596ab4630fcd03ac8eb31 (diff) | |
parent | 48206a62c9ac5f9d1794bb7f3e15fdfec0825cc8 (diff) | |
download | latinime-4d67480fe297f3b9265fcbeb1d1651b910bd96c2.tar.gz latinime-4d67480fe297f3b9265fcbeb1d1651b910bd96c2.tar.xz latinime-4d67480fe297f3b9265fcbeb1d1651b910bd96c2.zip |
Merge remote branch 'goog/master' into mastermerge
Conflicts:
java/src/com/android/inputmethod/latin/LatinIME.java
Change-Id: I8fe443f434ced0cf18b1aa86e38fb8913a5b75c2
Diffstat (limited to 'java/src/com/android/inputmethod/latin/PointerTracker.java')
-rw-r--r-- | java/src/com/android/inputmethod/latin/PointerTracker.java | 459 |
1 files changed, 459 insertions, 0 deletions
diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java new file mode 100644 index 000000000..2685e87c6 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/PointerTracker.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.inputmethod.latin; + +import com.android.inputmethod.latin.LatinKeyboardBaseView.OnKeyboardActionListener; +import com.android.inputmethod.latin.LatinKeyboardBaseView.UIHandler; + +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.Keyboard.Key; +import android.util.Log; +import android.view.ViewConfiguration; + +public class PointerTracker { + private static final String TAG = "PointerTracker"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_MOVE = DEBUG && true; + + public interface UIProxy { + public void invalidateKey(Key key); + public void showPreview(int keyIndex, PointerTracker tracker); + } + + public final int mPointerId; + + // Timing constants + private static final int REPEAT_START_DELAY = 400; + /* package */ static final int REPEAT_INTERVAL = 50; // ~20 keys per second + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + private static final int MULTITAP_INTERVAL = 800; // milliseconds + private static final int KEY_DEBOUNCE_TIME = 70; + + // Miscellaneous constants + private static final int NOT_A_KEY = LatinKeyboardBaseView.NOT_A_KEY; + private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE }; + + private final UIProxy mProxy; + private final UIHandler mHandler; + private final KeyDetector mKeyDetector; + private OnKeyboardActionListener mListener; + + private Key[] mKeys; + private int mKeyDebounceThresholdSquared = -1; + + private int mCurrentKey = NOT_A_KEY; + private int mStartX; + private int mStartY; + private long mDownTime; + + // true if event is already translated to a key action (long press or mini-keyboard) + private boolean mKeyAlreadyProcessed; + + // for move de-bouncing + private int mLastCodeX; + private int mLastCodeY; + private int mLastX; + private int mLastY; + + // for time de-bouncing + private int mLastKey; + private long mLastKeyTime; + private long mLastMoveTime; + private long mCurrentKeyTime; + + // For multi-tap + private int mLastSentIndex; + private int mTapCount; + private long mLastTapTime; + private boolean mInMultiTap; + private final StringBuilder mPreviewLabel = new StringBuilder(1); + + // pressed key + private int mPreviousKey = NOT_A_KEY; + + public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy) { + if (proxy == null || handler == null || keyDetector == null) + throw new NullPointerException(); + mPointerId = id; + mProxy = proxy; + mHandler = handler; + mKeyDetector = keyDetector; + resetMultiTap(); + } + + public void setOnKeyboardActionListener(OnKeyboardActionListener listener) { + mListener = listener; + } + + public void setKeyboard(Key[] keys, float hysteresisPixel) { + if (keys == null || hysteresisPixel < 1.0f) + throw new IllegalArgumentException(); + mKeys = keys; + mKeyDebounceThresholdSquared = (int)(hysteresisPixel * hysteresisPixel); + } + + private boolean isValidKeyIndex(int keyIndex) { + return keyIndex >= 0 && keyIndex < mKeys.length; + } + + public Key getKey(int keyIndex) { + return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null; + } + + public boolean isModifier() { + Key key = getKey(mCurrentKey); + if (key == null) + return false; + int primaryCode = key.codes[0]; + // TODO: KEYCODE_MODE_CHANGE (symbol) will be also a modifier key + return primaryCode == Keyboard.KEYCODE_SHIFT; + } + + public void updateKey(int keyIndex) { + if (mKeyAlreadyProcessed) + return; + int oldKeyIndex = mPreviousKey; + mPreviousKey = keyIndex; + if (keyIndex != oldKeyIndex) { + if (isValidKeyIndex(oldKeyIndex)) { + // if new key index is not a key, old key was just released inside of the key. + final boolean inside = (keyIndex == NOT_A_KEY); + mKeys[oldKeyIndex].onReleased(inside); + mProxy.invalidateKey(mKeys[oldKeyIndex]); + } + if (isValidKeyIndex(keyIndex)) { + mKeys[keyIndex].onPressed(); + mProxy.invalidateKey(mKeys[keyIndex]); + } + } + } + + public void setAlreadyProcessed() { + mKeyAlreadyProcessed = true; + } + + public void onDownEvent(int x, int y, long eventTime) { + int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); + mCurrentKey = keyIndex; + mStartX = x; + mStartY = y; + mDownTime = eventTime; + mKeyAlreadyProcessed = false; + startMoveDebouncing(x, y); + startTimeDebouncing(eventTime); + checkMultiTap(eventTime, keyIndex); + if (mListener != null) { + int primaryCode = isValidKeyIndex(keyIndex) ? mKeys[keyIndex].codes[0] : 0; + mListener.onPress(primaryCode); + } + if (isValidKeyIndex(keyIndex)) { + if (mKeys[keyIndex].repeatable) { + repeatKey(keyIndex); + mHandler.startKeyRepeatTimer(REPEAT_START_DELAY, keyIndex, this); + } + mHandler.startLongPressTimer(LONGPRESS_TIMEOUT, keyIndex, this); + } + showKeyPreviewAndUpdateKey(keyIndex); + updateMoveDebouncing(x, y); + if (DEBUG) + debugLog("onDownEvent:", x, y); + } + + public void onMoveEvent(int x, int y, long eventTime) { + if (mKeyAlreadyProcessed) + return; + int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); + if (isValidKeyIndex(keyIndex)) { + if (mCurrentKey == NOT_A_KEY) { + updateTimeDebouncing(eventTime); + mCurrentKey = keyIndex; + mHandler.startLongPressTimer(LONGPRESS_TIMEOUT, keyIndex, this); + } else if (isMinorMoveBounce(x, y, keyIndex, mCurrentKey)) { + updateTimeDebouncing(eventTime); + } else { + resetMultiTap(); + resetTimeDebouncing(eventTime, mCurrentKey); + resetMoveDebouncing(); + mCurrentKey = keyIndex; + mHandler.startLongPressTimer(LONGPRESS_TIMEOUT, keyIndex, this); + } + } else { + if (mCurrentKey != NOT_A_KEY) { + updateTimeDebouncing(eventTime); + mCurrentKey = keyIndex; + mHandler.cancelLongPressTimer(); + } else if (isMinorMoveBounce(x, y, keyIndex, mCurrentKey)) { + updateTimeDebouncing(eventTime); + } else { + resetMultiTap(); + resetTimeDebouncing(eventTime, mCurrentKey); + resetMoveDebouncing(); + mCurrentKey = keyIndex; + mHandler.cancelLongPressTimer(); + } + } + /* + * While time debouncing is in effect, mCurrentKey holds the new key and this tracker + * holds the last key. At ACTION_UP event if time debouncing will be in effect + * eventually, the last key should be sent as the result. In such case mCurrentKey + * should not be showed as popup preview. + */ + showKeyPreviewAndUpdateKey(isMinorTimeBounce() ? mLastKey : mCurrentKey); + updateMoveDebouncing(x, y); + if (DEBUG_MOVE) + debugLog("onMoveEvent:", x, y); + } + + public void onUpEvent(int x, int y, long eventTime) { + if (mKeyAlreadyProcessed) + return; + if (DEBUG) + debugLog("onUpEvent :", x, y); + int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(x, y, null); + boolean wasInKeyRepeat = mHandler.isInKeyRepeat(); + mHandler.cancelKeyTimers(); + mHandler.cancelPopupPreview(); + if (isMinorMoveBounce(x, y, keyIndex, mCurrentKey)) { + updateTimeDebouncing(eventTime); + } else { + resetMultiTap(); + resetTimeDebouncing(eventTime, mCurrentKey); + mCurrentKey = keyIndex; + } + if (isMinorTimeBounce()) { + mCurrentKey = mLastKey; + x = mLastCodeX; + y = mLastCodeY; + } + showKeyPreviewAndUpdateKey(NOT_A_KEY); + // If we're not on a repeating key (which sends on a DOWN event) + if (!wasInKeyRepeat) { + detectAndSendKey(mCurrentKey, (int)x, (int)y, eventTime); + } + if (isValidKeyIndex(keyIndex)) + mProxy.invalidateKey(mKeys[keyIndex]); + } + + public void onCancelEvent(int x, int y, long eventTime) { + if (DEBUG) + debugLog("onCancelEvt:", x, y); + mHandler.cancelKeyTimers(); + mHandler.cancelPopupPreview(); + showKeyPreviewAndUpdateKey(NOT_A_KEY); + int keyIndex = mCurrentKey; + if (isValidKeyIndex(keyIndex)) + mProxy.invalidateKey(mKeys[keyIndex]); + } + + public void repeatKey(int keyIndex) { + Key key = getKey(keyIndex); + if (key != null) { + // While key is repeating, because there is no need to handle multi-tap key, we can + // pass -1 as eventTime argument. + detectAndSendKey(keyIndex, key.x, key.y, -1); + } + } + + public int getLastX() { + return mLastX; + } + + public int getLastY() { + return mLastY; + } + + public long getDownTime() { + return mDownTime; + } + + // These package scope methods are only for debugging purpose. + /* package */ int getStartX() { + return mStartX; + } + + /* package */ int getStartY() { + return mStartY; + } + + private void startMoveDebouncing(int x, int y) { + mLastCodeX = x; + mLastCodeY = y; + } + + private void updateMoveDebouncing(int x, int y) { + mLastX = x; + mLastY = y; + } + + private void resetMoveDebouncing() { + mLastCodeX = mLastX; + mLastCodeY = mLastY; + } + + private boolean isMinorMoveBounce(int x, int y, int newKey, int curKey) { + if (mKeys == null || mKeyDebounceThresholdSquared < 0) + throw new IllegalStateException("keyboard and/or hysteresis not set"); + if (newKey == curKey) { + return true; + } else if (isValidKeyIndex(curKey)) { + return getSquareDistanceToKeyEdge(x, y, mKeys[curKey]) + < mKeyDebounceThresholdSquared; + } else { + return false; + } + } + + private static int getSquareDistanceToKeyEdge(int x, int y, Key key) { + final int left = key.x; + final int right = key.x + key.width; + final int top = key.y; + final int bottom = key.y + key.height; + final int edgeX = x < left ? left : (x > right ? right : x); + final int edgeY = y < top ? top : (y > bottom ? bottom : y); + final int dx = x - edgeX; + final int dy = y - edgeY; + return dx * dx + dy * dy; + } + + private void startTimeDebouncing(long eventTime) { + mLastKey = NOT_A_KEY; + mLastKeyTime = 0; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + private void updateTimeDebouncing(long eventTime) { + mCurrentKeyTime += eventTime - mLastMoveTime; + mLastMoveTime = eventTime; + } + + private void resetTimeDebouncing(long eventTime, int currentKey) { + mLastKey = currentKey; + mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime; + mCurrentKeyTime = 0; + mLastMoveTime = eventTime; + } + + private boolean isMinorTimeBounce() { + return mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < KEY_DEBOUNCE_TIME + && mLastKey != NOT_A_KEY; + } + + private void showKeyPreviewAndUpdateKey(int keyIndex) { + updateKey(keyIndex); + if (!isModifier()) + mProxy.showPreview(keyIndex, this); + } + + private void detectAndSendKey(int index, int x, int y, long eventTime) { + if (isValidKeyIndex(index)) { + final Key key = mKeys[index]; + OnKeyboardActionListener listener = mListener; + if (key.text != null) { + if (listener != null) { + listener.onText(key.text); + listener.onRelease(NOT_A_KEY); + } + } else { + int code = key.codes[0]; + //TextEntryState.keyPressedAt(key, x, y); + int[] codes = mKeyDetector.newCodeArray(); + mKeyDetector.getKeyIndexAndNearbyCodes(x, y, codes); + // Multi-tap + if (mInMultiTap) { + if (mTapCount != -1) { + mListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE, x, y); + } else { + mTapCount = 0; + } + code = key.codes[mTapCount]; + } + /* + * Swap the first and second values in the codes array if the primary code is not + * the first value but the second value in the array. This happens when key + * debouncing is in effect. + */ + if (codes.length >= 2 && codes[0] != code && codes[1] == code) { + codes[1] = codes[0]; + codes[0] = code; + } + if (listener != null) { + listener.onKey(code, codes, x, y); + listener.onRelease(code); + } + } + mLastSentIndex = index; + mLastTapTime = eventTime; + } + } + + /** + * Handle multi-tap keys by producing the key label for the current multi-tap state. + */ + public CharSequence getPreviewText(Key key) { + if (mInMultiTap) { + // Multi-tap + mPreviewLabel.setLength(0); + mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]); + return mPreviewLabel; + } else { + return key.label; + } + } + + private void resetMultiTap() { + mLastSentIndex = NOT_A_KEY; + mTapCount = 0; + mLastTapTime = -1; + mInMultiTap = false; + } + + private void checkMultiTap(long eventTime, int keyIndex) { + Key key = getKey(keyIndex); + if (key == null) + return; + + final boolean isMultiTap = + (eventTime < mLastTapTime + MULTITAP_INTERVAL && keyIndex == mLastSentIndex); + if (key.codes.length > 1) { + mInMultiTap = true; + if (isMultiTap) { + mTapCount = (mTapCount + 1) % key.codes.length; + return; + } else { + mTapCount = -1; + return; + } + } + if (!isMultiTap) { + resetMultiTap(); + } + } + + private void debugLog(String title, int x, int y) { + Key key = getKey(mCurrentKey); + final String code; + if (key == null) { + code = "----"; + } else { + int primaryCode = key.codes[0]; + code = String.format((primaryCode < 0) ? "%4d" : "0x%02x", primaryCode); + } + Log.d(TAG, String.format("%s [%d] %3d,%3d %s %s", title, mPointerId, x, y, code, + isModifier() ? "modifier" : "")); + } +}
\ No newline at end of file |