aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/PointerTracker.java
diff options
context:
space:
mode:
authorsatok <satok@google.com>2010-09-03 22:57:28 +0900
committersatok <satok@google.com>2010-09-03 22:57:28 +0900
commit4d67480fe297f3b9265fcbeb1d1651b910bd96c2 (patch)
tree56ec6666d6c13e1daefd43ed9fbe44988d42fb8c /java/src/com/android/inputmethod/latin/PointerTracker.java
parent17fe18350fe5b411f78596ab4630fcd03ac8eb31 (diff)
parent48206a62c9ac5f9d1794bb7f3e15fdfec0825cc8 (diff)
downloadlatinime-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.java459
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