diff options
Diffstat (limited to 'java/src')
17 files changed, 1410 insertions, 791 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java index 7199550a9..89adc15f2 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java @@ -16,11 +16,15 @@ package com.android.inputmethod.accessibility; +import android.content.Context; import android.content.SharedPreferences; import android.inputmethodservice.InputMethodService; +import android.media.AudioManager; import android.os.Looper; import android.os.Message; +import android.os.Vibrator; import android.text.TextUtils; +import android.view.KeyEvent; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -38,8 +42,14 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi */ private static final long DELAY_NO_HOVER_SELECTION = 250; - private InputMethodService mInputMethod; + /** + * Duration of the key click vibration in milliseconds. + */ + private static final long VIBRATE_KEY_CLICK = 50; + private InputMethodService mInputMethod; + private Vibrator mVibrator; + private AudioManager mAudioManager; private AccessibilityHandler mAccessibilityHandler; private static class AccessibilityHandler @@ -84,6 +94,8 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) { mInputMethod = inputMethod; + mVibrator = (Vibrator) inputMethod.getSystemService(Context.VIBRATOR_SERVICE); + mAudioManager = (AudioManager) inputMethod.getSystemService(Context.AUDIO_SERVICE); mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper()); } @@ -107,6 +119,35 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi } /** + * Handle flick gestures by mapping them to directional pad keys. + */ + @Override + public void onFlickGesture(int direction) { + final int keyEventCode; + + switch (direction) { + case FlickGestureDetector.FLICK_LEFT: + sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT); + break; + case FlickGestureDetector.FLICK_RIGHT: + sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT); + break; + } + } + + /** + * Provide haptic feedback and send the specified keyCode to the input + * connection as a pair of down/up events. + * + * @param keyCode + */ + private void sendDownUpKeyEvents(int keyCode) { + mVibrator.vibrate(VIBRATE_KEY_CLICK); + mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); + mInputMethod.sendDownUpKeyEvents(keyCode); + } + + /** * When Accessibility is turned on, notifies the user that they are not * currently hovering above a key. By default this will speak the currently * entered text. diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java index 12c59d0fc..c1e92bec8 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java @@ -34,4 +34,15 @@ public interface AccessibleKeyboardActionListener { * @param primaryCode the code of the key that was hovered over */ public void onHoverExit(int primaryCode); + + /** + * @param direction the direction of the flick gesture, one of + * <ul> + * <li>{@link FlickGestureDetector#FLICK_UP} + * <li>{@link FlickGestureDetector#FLICK_DOWN} + * <li>{@link FlickGestureDetector#FLICK_LEFT} + * <li>{@link FlickGestureDetector#FLICK_RIGHT} + * </ul> + */ + public void onFlickGesture(int direction); } diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index 96f7fc9f2..38d904a94 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -29,7 +29,7 @@ import com.android.inputmethod.compat.AccessibilityEventCompatUtils; import com.android.inputmethod.compat.MotionEventCompatUtils; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.KeyDetector; -import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.LatinKeyboardBaseView; import com.android.inputmethod.keyboard.PointerTracker; public class AccessibleKeyboardViewProxy { @@ -40,8 +40,9 @@ public class AccessibleKeyboardViewProxy { private static final long DELAY_KEY_PRESS = 10; private int mScaledEdgeSlop; - private KeyboardView mView; + private LatinKeyboardBaseView mView; private AccessibleKeyboardActionListener mListener; + private FlickGestureDetector mGestureDetector; private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY; private int mLastX = -1; @@ -56,7 +57,7 @@ public class AccessibleKeyboardViewProxy { return sInstance; } - public static void setView(KeyboardView view) { + public static void setView(LatinKeyboardBaseView view) { sInstance.mView = view; } @@ -71,6 +72,7 @@ public class AccessibleKeyboardViewProxy { paint.setAntiAlias(true); paint.setColor(Color.YELLOW); + mGestureDetector = new KeyboardFlickGestureDetector(context); mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop(); } @@ -110,7 +112,10 @@ public class AccessibleKeyboardViewProxy { * @return {@code true} if the event is handled */ public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) { - return onTouchExplorationEvent(event, tracker); + if (mGestureDetector.onHoverEvent(event, this, tracker)) + return true; + + return onHoverEventInternal(event, tracker); } public boolean dispatchTouchEvent(MotionEvent event) { @@ -128,7 +133,7 @@ public class AccessibleKeyboardViewProxy { * @param event The touch exploration hover event. * @return {@code true} if the event was handled */ - private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) { + /*package*/ boolean onHoverEventInternal(MotionEvent event, PointerTracker tracker) { final int x = (int) event.getX(); final int y = (int) event.getY(); @@ -198,4 +203,18 @@ public class AccessibleKeyboardViewProxy { tracker.onDownEvent(x, y, eventTime, null); tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null); } + + private class KeyboardFlickGestureDetector extends FlickGestureDetector { + public KeyboardFlickGestureDetector(Context context) { + super(context); + } + + @Override + public boolean onFlick(MotionEvent e1, MotionEvent e2, int direction) { + if (mListener != null) { + mListener.onFlickGesture(direction); + } + return true; + } + } } diff --git a/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java new file mode 100644 index 000000000..9d99e3131 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.accessibility; + +import android.content.Context; +import android.os.Message; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.inputmethod.compat.MotionEventCompatUtils; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.StaticInnerHandlerWrapper; + +/** + * Detects flick gestures within a stream of hover events. + * <p> + * A flick gesture is defined as a stream of hover events with the following + * properties: + * <ul> + * <li>Begins with a {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event + * <li>Contains any number of {@link MotionEventCompatUtils#ACTION_HOVER_MOVE} + * events + * <li>Ends with a {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event + * <li>Maximum duration of 250 milliseconds + * <li>Minimum distance between enter and exit points must be at least equal to + * scaled double tap slop (see + * {@link ViewConfiguration#getScaledDoubleTapSlop()}) + * </ul> + * <p> + * Initial enter events are intercepted and cached until the stream fails to + * satisfy the constraints defined above, at which point the cached enter event + * is sent to its source {@link AccessibleKeyboardViewProxy} and subsequent move + * and exit events are ignored. + */ +public abstract class FlickGestureDetector { + public static final int FLICK_UP = 0; + public static final int FLICK_RIGHT = 1; + public static final int FLICK_LEFT = 2; + public static final int FLICK_DOWN = 3; + + private final FlickHandler mFlickHandler; + private final int mFlickRadiusSquare; + + private AccessibleKeyboardViewProxy mCachedView; + private PointerTracker mCachedTracker; + private MotionEvent mCachedHoverEnter; + + private static class FlickHandler extends StaticInnerHandlerWrapper<FlickGestureDetector> { + private static final int MSG_FLICK_TIMEOUT = 1; + + /** The maximum duration of a flick gesture in milliseconds. */ + private static final int DELAY_FLICK_TIMEOUT = 250; + + public FlickHandler(FlickGestureDetector outerInstance) { + super(outerInstance); + } + + @Override + public void handleMessage(Message msg) { + final FlickGestureDetector gestureDetector = getOuterInstance(); + + switch (msg.what) { + case MSG_FLICK_TIMEOUT: + gestureDetector.clearFlick(true); + } + } + + public void startFlickTimeout() { + cancelFlickTimeout(); + sendEmptyMessageDelayed(MSG_FLICK_TIMEOUT, DELAY_FLICK_TIMEOUT); + } + + public void cancelFlickTimeout() { + removeMessages(MSG_FLICK_TIMEOUT); + } + } + + /** + * Creates a new flick gesture detector. + * + * @param context The parent context. + */ + public FlickGestureDetector(Context context) { + final int doubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + + mFlickHandler = new FlickHandler(this); + mFlickRadiusSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * Processes motion events to detect flick gestures. + * + * @param event The current event. + * @param view The source of the event. + * @param tracker A pointer tracker for the event. + * @return {@code true} if the event was handled. + */ + public boolean onHoverEvent(MotionEvent event, AccessibleKeyboardViewProxy view, + PointerTracker tracker) { + // Always cache and consume the first hover event. + if (event.getAction() == MotionEventCompatUtils.ACTION_HOVER_ENTER) { + mCachedView = view; + mCachedTracker = tracker; + mCachedHoverEnter = MotionEvent.obtain(event); + mFlickHandler.startFlickTimeout(); + return true; + } + + // Stop if the event has already been canceled. + if (mCachedHoverEnter == null) { + return false; + } + + final float distanceSquare = calculateDistanceSquare(mCachedHoverEnter, event); + final long timeout = event.getEventTime() - mCachedHoverEnter.getEventTime(); + + switch (event.getAction()) { + case MotionEventCompatUtils.ACTION_HOVER_MOVE: + // Consume all valid move events before timeout. + return true; + case MotionEventCompatUtils.ACTION_HOVER_EXIT: + // Ignore exit events outside the flick radius. + if (distanceSquare < mFlickRadiusSquare) { + clearFlick(true); + return false; + } else { + return dispatchFlick(mCachedHoverEnter, event); + } + default: + return false; + } + } + + /** + * Clears the cached flick information and optionally forwards the event to + * the source view's internal hover event handler. + * + * @param sendCachedEvent Set to {@code true} to forward the hover event to + * the source view. + */ + private void clearFlick(boolean sendCachedEvent) { + mFlickHandler.cancelFlickTimeout(); + + if (mCachedHoverEnter != null) { + if (sendCachedEvent) { + mCachedView.onHoverEventInternal(mCachedHoverEnter, mCachedTracker); + } + mCachedHoverEnter.recycle(); + mCachedHoverEnter = null; + } + + mCachedTracker = null; + mCachedView = null; + } + + /** + * Computes the direction of a flick gesture and forwards it to + * {@link #onFlick(MotionEvent, MotionEvent, int)} for handling. + * + * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event + * where the flick started. + * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event + * where the flick ended. + * @return {@code true} if the flick event was handled. + */ + private boolean dispatchFlick(MotionEvent e1, MotionEvent e2) { + clearFlick(false); + + final float dX = e2.getX() - e1.getX(); + final float dY = e2.getY() - e1.getY(); + final int direction; + + if (dY > dX) { + if (dY > -dX) { + direction = FLICK_DOWN; + } else { + direction = FLICK_LEFT; + } + } else { + if (dY > -dX) { + direction = FLICK_RIGHT; + } else { + direction = FLICK_UP; + } + } + + return onFlick(e1, e2, direction); + } + + private float calculateDistanceSquare(MotionEvent e1, MotionEvent e2) { + final float dX = e2.getX() - e1.getX(); + final float dY = e2.getY() - e1.getY(); + return (dX * dX) + (dY * dY); + } + + /** + * Handles a detected flick gesture. + * + * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event + * where the flick started. + * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event + * where the flick ended. + * @param direction The direction of the flick event, one of: + * <ul> + * <li>{@link #FLICK_UP} + * <li>{@link #FLICK_DOWN} + * <li>{@link #FLICK_LEFT} + * <li>{@link #FLICK_RIGHT} + * </ul> + * @return {@code true} if the flick event was handled. + */ + public abstract boolean onFlick(MotionEvent e1, MotionEvent e2, int direction); +} diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 203cde915..f3e023d3e 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -17,7 +17,6 @@ package com.android.inputmethod.keyboard; import android.content.Context; -import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -32,39 +31,24 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Message; import android.util.AttributeSet; -import android.util.Log; import android.util.TypedValue; -import android.view.GestureDetector; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; -import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.widget.PopupWindow; import android.widget.TextView; -import com.android.inputmethod.accessibility.AccessibilityUtils; -import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; import com.android.inputmethod.compat.FrameLayoutCompatUtils; -import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder; -import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; -import com.android.inputmethod.keyboard.internal.SwipeTracker; import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.StaticInnerHandlerWrapper; -import java.util.ArrayList; import java.util.HashMap; -import java.util.WeakHashMap; /** - * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key - * presses and touch movements. + * A view that renders a virtual {@link Keyboard}. * * @attr ref R.styleable#KeyboardView_backgroundDimAmount * @attr ref R.styleable#KeyboardView_keyBackground - * @attr ref R.styleable#KeyboardView_keyHysteresisDistance * @attr ref R.styleable#KeyboardView_keyLetterRatio * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio * @attr ref R.styleable#KeyboardView_keyLabelRatio @@ -85,29 +69,17 @@ import java.util.WeakHashMap; * @attr ref R.styleable#KeyboardView_keyHintLabelColor * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor - * @attr ref R.styleable#KeyboardView_verticalCorrection - * @attr ref R.styleable#KeyboardView_popupLayout * @attr ref R.styleable#KeyboardView_shadowColor * @attr ref R.styleable#KeyboardView_shadowRadius */ public class KeyboardView extends View implements PointerTracker.UIProxy { - private static final String TAG = KeyboardView.class.getSimpleName(); private static final boolean DEBUG_KEYBOARD_GRID = false; - private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true; - private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true; - - // Timing constants - private final int mKeyRepeatInterval; - // Miscellaneous constants private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; // XML attribute private final float mBackgroundDimAmount; - private final float mKeyHysteresisDistance; - private final float mVerticalCorrection; - private final int mPopupLayout; // HORIZONTAL ELLIPSIS "...", character for popup hint. private static final String POPUP_HINT_CHAR = "\u2026"; @@ -124,32 +96,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { private int mDelayAfterPreview; private ViewGroup mPreviewPlacer; - // Mini keyboard - private PopupWindow mPopupWindow; - private PopupPanel mPopupMiniKeyboardPanel; - private final WeakHashMap<Key, PopupPanel> mPopupPanelCache = - new WeakHashMap<Key, PopupPanel>(); - - /** Listener for {@link KeyboardActionListener}. */ - private KeyboardActionListener mKeyboardActionListener; - - private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); - - // TODO: Let the PointerTracker class manage this pointer queue - private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue(); - - private final boolean mHasDistinctMultitouch; - private int mOldPointerCount = 1; - private int mOldKeyIndex; - - protected KeyDetector mKeyDetector = new KeyDetector(); - - // Swipe gesture detector - protected GestureDetector mGestureDetector; - private final SwipeTracker mSwipeTracker = new SwipeTracker(); - private final int mSwipeThreshold; - private final boolean mDisambiguateSwipe; - // Drawing /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/ private boolean mDrawPending; @@ -182,12 +128,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { public static class UIHandler extends StaticInnerHandlerWrapper<KeyboardView> { private static final int MSG_SHOW_KEY_PREVIEW = 1; private static final int MSG_DISMISS_KEY_PREVIEW = 2; - private static final int MSG_REPEAT_KEY = 3; - private static final int MSG_LONGPRESS_KEY = 4; - private static final int MSG_LONGPRESS_SHIFT_KEY = 5; - private static final int MSG_IGNORE_DOUBLE_TAP = 6; - - private boolean mInKeyRepeat; public UIHandler(KeyboardView outerInstance) { super(outerInstance); @@ -204,16 +144,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { case MSG_DISMISS_KEY_PREVIEW: keyboardView.mPreviewText.setVisibility(View.INVISIBLE); break; - case MSG_REPEAT_KEY: - tracker.onRepeatKey(msg.arg1); - startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, msg.arg1, tracker); - break; - case MSG_LONGPRESS_KEY: - keyboardView.openMiniKeyboardIfRequired(msg.arg1, tracker); - break; - case MSG_LONGPRESS_SHIFT_KEY: - keyboardView.onLongPressShiftKey(tracker); - break; } } @@ -249,55 +179,7 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { removeMessages(MSG_DISMISS_KEY_PREVIEW); } - public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { - mInKeyRepeat = true; - sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); - } - - public void cancelKeyRepeatTimer() { - mInKeyRepeat = false; - removeMessages(MSG_REPEAT_KEY); - } - - public boolean isInKeyRepeat() { - return mInKeyRepeat; - } - - public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { - cancelLongPressTimers(); - sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); - } - - public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) { - cancelLongPressTimers(); - if (ENABLE_CAPSLOCK_BY_LONGPRESS) { - sendMessageDelayed( - obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay); - } - } - - public void cancelLongPressTimers() { - removeMessages(MSG_LONGPRESS_KEY); - removeMessages(MSG_LONGPRESS_SHIFT_KEY); - } - - public void cancelKeyTimers() { - cancelKeyRepeatTimer(); - cancelLongPressTimers(); - removeMessages(MSG_IGNORE_DOUBLE_TAP); - } - - public void startIgnoringDoubleTap() { - sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP), - ViewConfiguration.getDoubleTapTimeout()); - } - - public boolean isIgnoringDoubleTap() { - return hasMessages(MSG_IGNORE_DOUBLE_TAP); - } - public void cancelAllMessages() { - cancelKeyTimers(); cancelAllShowKeyPreviews(); cancelAllDismissKeyPreviews(); } @@ -394,6 +276,8 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { public int mKeyLetterSize; public final int[] mCoordinates = new int[2]; + private static final int PREVIEW_ALPHA = 240; + public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) { mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground); mPreviewLeftBackground = a.getDrawable( @@ -402,6 +286,9 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { R.styleable.KeyboardView_keyPreviewRightBackground); mPreviewSpacebarBackground = a.getDrawable( R.styleable.KeyboardView_keyPreviewSpacebarBackground); + setAlpha(mPreviewBackground, PREVIEW_ALPHA); + setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); + setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); mPreviewOffset = a.getDimensionPixelOffset( R.styleable.KeyboardView_keyPreviewOffset, 0); mPreviewHeight = a.getDimensionPixelSize( @@ -417,6 +304,12 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); } + + private static void setAlpha(Drawable drawable, int alpha) { + if (drawable == null) + return; + drawable.setAlpha(alpha); + } } public KeyboardView(Context context, AttributeSet attrs) { @@ -431,10 +324,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { mKeyDrawParams = new KeyDrawParams(a); mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams); - mKeyHysteresisDistance = a.getDimensionPixelOffset( - R.styleable.KeyboardView_keyHysteresisDistance, 0); - mVerticalCorrection = a.getDimensionPixelOffset( - R.styleable.KeyboardView_verticalCorrection, 0); final int previewLayout = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); if (previewLayout != 0) { mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null); @@ -442,7 +331,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { mPreviewText = null; mShowKeyPreviewPopup = false; } - mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0); // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f); a.recycle(); @@ -455,82 +343,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { mPaint.setAntiAlias(true); mPaint.setTextAlign(Align.CENTER); mPaint.setAlpha(255); - - mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); - // TODO: Refer to frameworks/base/core/res/res/values/config.xml - mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); - - GestureDetector.SimpleOnGestureListener listener = - new GestureDetector.SimpleOnGestureListener() { - private boolean mProcessingShiftDoubleTapEvent = false; - - @Override - public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, - float velocityY) { - final float absX = Math.abs(velocityX); - final float absY = Math.abs(velocityY); - float deltaY = me2.getY() - me1.getY(); - int travelY = getHeight() / 2; // Half the keyboard height - mSwipeTracker.computeCurrentVelocity(1000); - final float endingVelocityY = mSwipeTracker.getYVelocity(); - if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { - if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { - onSwipeDown(); - return true; - } - } - return false; - } - - @Override - public boolean onDoubleTap(MotionEvent firstDown) { - if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard - && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) { - final int pointerIndex = firstDown.getActionIndex(); - final int id = firstDown.getPointerId(pointerIndex); - final PointerTracker tracker = getPointerTracker(id); - // If the first down event is on shift key. - if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) { - mProcessingShiftDoubleTapEvent = true; - return true; - } - } - mProcessingShiftDoubleTapEvent = false; - return false; - } - - @Override - public boolean onDoubleTapEvent(MotionEvent secondTap) { - if (mProcessingShiftDoubleTapEvent - && secondTap.getAction() == MotionEvent.ACTION_DOWN) { - final MotionEvent secondDown = secondTap; - final int pointerIndex = secondDown.getActionIndex(); - final int id = secondDown.getPointerId(pointerIndex); - final PointerTracker tracker = getPointerTracker(id); - // If the second down event is also on shift key. - if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) { - // Detected a double tap on shift key. If we are in the ignoring double tap - // mode, it means we have already turned off caps lock in - // {@link KeyboardSwitcher#onReleaseShift} . - final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap(); - if (!ignoringDoubleTap) - onDoubleTapShiftKey(tracker); - return true; - } - // Otherwise these events should not be handled as double tap. - mProcessingShiftDoubleTapEvent = false; - } - return mProcessingShiftDoubleTapEvent; - } - }; - - final boolean ignoreMultitouch = true; - mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); - mGestureDetector.setIsLongpressEnabled(false); - - mHasDistinctMultitouch = context.getPackageManager() - .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); - mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); } // Read fraction value in TypedArray as float. @@ -538,26 +350,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { return a.getFraction(index, 1000, 1000, 1) / 1000.0f; } - public void startIgnoringDoubleTap() { - if (ENABLE_CAPSLOCK_BY_DOUBLETAP) - mHandler.startIgnoringDoubleTap(); - } - - public void setOnKeyboardActionListener(KeyboardActionListener listener) { - mKeyboardActionListener = listener; - for (PointerTracker tracker : mPointerTrackers) { - tracker.setOnKeyboardActionListener(listener); - } - } - - /** - * Returns the {@link KeyboardActionListener} object. - * @return the listener attached to this keyboard - */ - protected KeyboardActionListener getOnKeyboardActionListener() { - return mKeyboardActionListener; - } - @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { // TODO: Should notify InputMethodService instead? @@ -572,24 +364,13 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { * @param keyboard the keyboard to display in this view */ public void setKeyboard(Keyboard keyboard) { - if (mKeyboard != null) { - dismissAllKeyPreviews(); - } // Remove any pending messages, except dismissing preview - mHandler.cancelKeyTimers(); mHandler.cancelAllShowKeyPreviews(); mKeyboard = keyboard; LatinImeLogger.onSetKeyboard(keyboard); - mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), - -getPaddingTop() + mVerticalCorrection); - for (PointerTracker tracker : mPointerTrackers) { - tracker.setKeyboard(keyboard, mKeyHysteresisDistance); - } requestLayout(); mKeyboardChanged = true; invalidateAllKeys(); - mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth()); - mPopupPanelCache.clear(); final int keyHeight = keyboard.getRowHeight() - keyboard.getVerticalGap(); mKeyDrawParams.updateKeyHeight(keyHeight); mKeyPreviewDrawParams.updateKeyHeight(keyHeight); @@ -605,15 +386,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { } /** - * Returns whether the device has distinct multi-touch panel. - * @return true if the device has distinct multi-touch panel. - */ - @Override - public boolean hasDistinctMultitouch() { - return mHasDistinctMultitouch; - } - - /** * Enables or disables the key feedback popup. This is a popup that shows a magnified * version of the depressed key. By default the preview is enabled. * @param previewEnabled whether or not to enable the key feedback preview @@ -634,23 +406,6 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { return mShowKeyPreviewPopup; } - /** - * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key - * codes for adjacent keys. When disabled, only the primary key code will be - * reported. - * @param enabled whether or not the proximity correction is enabled - */ - public void setProximityCorrectionEnabled(boolean enabled) { - mKeyDetector.setProximityCorrectionEnabled(enabled); - } - - /** - * Returns true if proximity correction is enabled. - */ - public boolean isProximityCorrectionEnabled() { - return mKeyDetector.isProximityCorrectionEnabled(); - } - @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Round up a little @@ -741,7 +496,7 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { } // Overlay a dark rectangle to dim the keyboard - if (mPopupMiniKeyboardPanel != null) { + if (needsToDimKeyboard()) { mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); canvas.drawRect(0, 0, width, height, mPaint); } @@ -751,6 +506,10 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { mDirtyRect.setEmpty(); } + protected boolean needsToDimKeyboard() { + return false; + } + private static void onBufferDrawKey(final Key key, final Canvas canvas, Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase) { final boolean debugShowAlign = LatinImeLogger.sVISUALDEBUG; @@ -1006,15 +765,7 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { canvas.translate(-x, -y); } - // TODO: clean up this method. - private void dismissAllKeyPreviews() { - for (PointerTracker tracker : mPointerTrackers) { - tracker.setReleasedKeyGraphics(); - dismissKeyPreview(tracker); - } - } - - public void cancelAllMessage() { + public void cancelAllMessages() { mHandler.cancelAllMessages(); } @@ -1029,6 +780,11 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { } @Override + public void cancelShowKeyPreview(PointerTracker tracker) { + mHandler.cancelShowKeyPreview(tracker); + } + + @Override public void dismissKeyPreview(PointerTracker tracker) { if (mShowKeyPreviewPopup) { mHandler.cancelShowKeyPreview(tracker); @@ -1150,266 +906,11 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { invalidate(mInvalidatedKeyRect); } - private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) { - // Check if we have a popup layout specified first. - if (mPopupLayout == 0) { - return false; - } - - final Key parentKey = tracker.getKey(keyIndex); - if (parentKey == null) - return false; - boolean result = onLongPress(parentKey, tracker); - if (result) { - dismissAllKeyPreviews(); - tracker.onLongPressed(mPointerQueue); - } - return result; - } - - private void onLongPressShiftKey(PointerTracker tracker) { - tracker.onLongPressed(mPointerQueue); - mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); - } - - private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { - // When shift key is double tapped, the first tap is correctly processed as usual tap. And - // the second tap is treated as this double tap event, so that we need not mark tracker - // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. - mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); - } - - // This default implementation returns a popup mini keyboard panel. - // A derived class may return a language switcher popup panel, for instance. - protected PopupPanel onCreatePopupPanel(Key parentKey) { - if (parentKey.mPopupCharacters == null) - return null; - - final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); - if (container == null) - throw new NullPointerException(); - - final PopupMiniKeyboardView miniKeyboardView = - (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view); - miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { - @Override - public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { - mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); - dismissMiniKeyboard(); - } - - @Override - public void onTextInput(CharSequence text) { - mKeyboardActionListener.onTextInput(text); - dismissMiniKeyboard(); - } - - @Override - public void onCancelInput() { - mKeyboardActionListener.onCancelInput(); - dismissMiniKeyboard(); - } - - @Override - public void onSwipeDown() { - // Nothing to do. - } - @Override - public void onPress(int primaryCode, boolean withSliding) { - mKeyboardActionListener.onPress(primaryCode, withSliding); - } - @Override - public void onRelease(int primaryCode, boolean withSliding) { - mKeyboardActionListener.onRelease(primaryCode, withSliding); - } - }); - - final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(), - parentKey, mKeyboard).build(); - miniKeyboardView.setKeyboard(keyboard); - - container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), - MEASURESPEC_UNSPECIFIED); - - return miniKeyboardView; - } - - /** - * Called when a key is long pressed. By default this will open mini keyboard associated - * with this key. - * @param parentKey the key that was long pressed - * @param tracker the pointer tracker which pressed the parent key - * @return true if the long press is handled, false otherwise. Subclasses should call the - * method on the base class if the subclass doesn't wish to handle the call. - */ - protected boolean onLongPress(Key parentKey, PointerTracker tracker) { - PopupPanel popupPanel = mPopupPanelCache.get(parentKey); - if (popupPanel == null) { - popupPanel = onCreatePopupPanel(parentKey); - if (popupPanel == null) - return false; - mPopupPanelCache.put(parentKey, popupPanel); - } - if (mPopupWindow == null) { - mPopupWindow = new PopupWindow(getContext()); - mPopupWindow.setBackgroundDrawable(null); - mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation); - // Allow popup window to be drawn off the screen. - mPopupWindow.setClippingEnabled(false); - } - mPopupMiniKeyboardPanel = popupPanel; - popupPanel.showPanel(this, parentKey, tracker, mPopupWindow); - - invalidateAllKeys(); - return true; - } - - private PointerTracker getPointerTracker(final int id) { - final ArrayList<PointerTracker> pointers = mPointerTrackers; - final KeyboardActionListener listener = mKeyboardActionListener; - - // Create pointer trackers until we can get 'id+1'-th tracker, if needed. - for (int i = pointers.size(); i <= id; i++) { - final PointerTracker tracker = - new PointerTracker(i, this, mHandler, mKeyDetector, this); - if (mKeyboard != null) - tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance); - if (listener != null) - tracker.setOnKeyboardActionListener(listener); - pointers.add(tracker); - } - - return pointers.get(id); - } - - public boolean isInSlidingKeyInput() { - if (mPopupMiniKeyboardPanel != null) { - return mPopupMiniKeyboardPanel.isInSlidingKeyInput(); - } else { - return mPointerQueue.isInSlidingKeyInput(); - } - } - - public int getPointerCount() { - return mOldPointerCount; - } - - @Override - public boolean onTouchEvent(MotionEvent me) { - final int action = me.getActionMasked(); - final int pointerCount = me.getPointerCount(); - final int oldPointerCount = mOldPointerCount; - mOldPointerCount = pointerCount; - - // TODO: cleanup this code into a multi-touch to single-touch event converter class? - // If the device does not have distinct multi-touch support panel, ignore all multi-touch - // events except a transition from/to single-touch. - if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { - return true; - } - - // Track the last few movements to look for spurious swipes. - mSwipeTracker.addMovement(me); - - // Gesture detector must be enabled only when mini-keyboard is not on the screen. - if (mPopupMiniKeyboardPanel == null && mGestureDetector != null - && mGestureDetector.onTouchEvent(me)) { - dismissAllKeyPreviews(); - mHandler.cancelKeyTimers(); - return true; - } - - final long eventTime = me.getEventTime(); - final int index = me.getActionIndex(); - final int id = me.getPointerId(index); - final int x = (int)me.getX(index); - final int y = (int)me.getY(index); - - // Needs to be called after the gesture detector gets a turn, as it may have displayed the - // mini keyboard - if (mPopupMiniKeyboardPanel != null) { - return mPopupMiniKeyboardPanel.onTouchEvent(me); - } - - if (mHandler.isInKeyRepeat()) { - final PointerTracker tracker = getPointerTracker(id); - // Key repeating timer will be canceled if 2 or more keys are in action, and current - // event (UP or DOWN) is non-modifier key. - if (pointerCount > 1 && !tracker.isModifier()) { - mHandler.cancelKeyRepeatTimer(); - } - // Up event will pass through. - } - - // TODO: cleanup this code into a multi-touch to single-touch event converter class? - // Translate mutli-touch event to single-touch events on the device that has no distinct - // multi-touch panel. - if (!mHasDistinctMultitouch) { - // Use only main (id=0) pointer tracker. - PointerTracker tracker = getPointerTracker(0); - if (pointerCount == 1 && oldPointerCount == 2) { - // Multi-touch to single touch transition. - // Send a down event for the latest pointer if the key is different from the - // previous key. - final int newKeyIndex = tracker.getKeyIndexOn(x, y); - if (mOldKeyIndex != newKeyIndex) { - tracker.onDownEvent(x, y, eventTime, null); - if (action == MotionEvent.ACTION_UP) - tracker.onUpEvent(x, y, eventTime, null); - } - } else if (pointerCount == 2 && oldPointerCount == 1) { - // Single-touch to multi-touch transition. - // Send an up event for the last pointer. - final int lastX = tracker.getLastX(); - final int lastY = tracker.getLastY(); - mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY); - tracker.onUpEvent(lastX, lastY, eventTime, null); - } else if (pointerCount == 1 && oldPointerCount == 1) { - tracker.onTouchEvent(action, x, y, eventTime, null); - } else { - Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount - + " (old " + oldPointerCount + ")"); - } - return true; - } - - final PointerTrackerQueue queue = mPointerQueue; - if (action == MotionEvent.ACTION_MOVE) { - for (int i = 0; i < pointerCount; i++) { - final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); - tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); - } - } else { - final PointerTracker tracker = getPointerTracker(id); - switch (action) { - case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: - tracker.onDownEvent(x, y, eventTime, queue); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_POINTER_UP: - tracker.onUpEvent(x, y, eventTime, queue); - break; - case MotionEvent.ACTION_CANCEL: - tracker.onCancelEvent(x, y, eventTime, queue); - break; - } - } - - return true; - } - - protected void onSwipeDown() { - mKeyboardActionListener.onSwipeDown(); - } - public void closing() { mPreviewText.setVisibility(View.GONE); - mHandler.cancelAllMessages(); + cancelAllMessages(); - dismissMiniKeyboard(); mDirtyRect.union(0, 0, getWidth(), getHeight()); - mPopupPanelCache.clear(); requestLayout(); } @@ -1423,51 +924,4 @@ public class KeyboardView extends View implements PointerTracker.UIProxy { super.onDetachedFromWindow(); closing(); } - - private boolean dismissMiniKeyboard() { - if (mPopupWindow != null && mPopupWindow.isShowing()) { - mPopupWindow.dismiss(); - mPopupMiniKeyboardPanel = null; - invalidateAllKeys(); - return true; - } - return false; - } - - public boolean handleBack() { - return dismissMiniKeyboard(); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent event) { - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event) - || super.dispatchTouchEvent(event); - } - - return super.dispatchTouchEvent(event); - } - - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - final PointerTracker tracker = getPointerTracker(0); - return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent( - event, tracker) || super.dispatchPopulateAccessibilityEvent(event); - } - - return super.dispatchPopulateAccessibilityEvent(event); - } - - public boolean onHoverEvent(MotionEvent event) { - // Since reflection doesn't support calling superclass methods, this - // method checks for the existence of onHoverEvent() in the View class - // before returning a value. - if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { - final PointerTracker tracker = getPointerTracker(0); - return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker); - } - - return false; - } } diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardBaseView.java new file mode 100644 index 000000000..0a7ba05c8 --- /dev/null +++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardBaseView.java @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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.keyboard; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.widget.PopupWindow; + +import com.android.inputmethod.accessibility.AccessibilityUtils; +import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; +import com.android.inputmethod.keyboard.internal.MiniKeyboardBuilder; +import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; +import com.android.inputmethod.keyboard.internal.SwipeTracker; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.StaticInnerHandlerWrapper; + +import java.util.ArrayList; +import java.util.WeakHashMap; + +/** + * A view that is responsible for detecting key presses and touch movements. + * + * @attr ref R.styleable#KeyboardView_keyHysteresisDistance + * @attr ref R.styleable#KeyboardView_verticalCorrection + * @attr ref R.styleable#KeyboardView_popupLayout + */ +public class LatinKeyboardBaseView extends KeyboardView { + private static final String TAG = LatinKeyboardBaseView.class.getSimpleName(); + + private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true; + private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true; + + // Timing constants + private final int mKeyRepeatInterval; + + // XML attribute + private final float mKeyHysteresisDistance; + private final float mVerticalCorrection; + private final int mPopupLayout; + + // Mini keyboard + private PopupWindow mPopupWindow; + private PopupPanel mPopupMiniKeyboardPanel; + private final WeakHashMap<Key, PopupPanel> mPopupPanelCache = + new WeakHashMap<Key, PopupPanel>(); + + /** Listener for {@link KeyboardActionListener}. */ + private KeyboardActionListener mKeyboardActionListener; + + private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>(); + + // TODO: Let the PointerTracker class manage this pointer queue + private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue(); + + private final boolean mHasDistinctMultitouch; + private int mOldPointerCount = 1; + private int mOldKeyIndex; + + protected KeyDetector mKeyDetector = new KeyDetector(); + + // Swipe gesture detector + protected GestureDetector mGestureDetector; + private final SwipeTracker mSwipeTracker = new SwipeTracker(); + private final int mSwipeThreshold; + private final boolean mDisambiguateSwipe; + + private final UIHandler mHandler = new UIHandler(this); + + public static class UIHandler extends StaticInnerHandlerWrapper<LatinKeyboardBaseView> { + private static final int MSG_REPEAT_KEY = 3; + private static final int MSG_LONGPRESS_KEY = 4; + private static final int MSG_LONGPRESS_SHIFT_KEY = 5; + private static final int MSG_IGNORE_DOUBLE_TAP = 6; + + private boolean mInKeyRepeat; + + public UIHandler(LatinKeyboardBaseView outerInstance) { + super(outerInstance); + } + + @Override + public void handleMessage(Message msg) { + final LatinKeyboardBaseView keyboardView = getOuterInstance(); + final PointerTracker tracker = (PointerTracker) msg.obj; + switch (msg.what) { + case MSG_REPEAT_KEY: + tracker.onRepeatKey(msg.arg1); + startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, msg.arg1, tracker); + break; + case MSG_LONGPRESS_KEY: + keyboardView.openMiniKeyboardIfRequired(msg.arg1, tracker); + break; + case MSG_LONGPRESS_SHIFT_KEY: + keyboardView.onLongPressShiftKey(tracker); + break; + } + } + + public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) { + mInKeyRepeat = true; + sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay); + } + + public void cancelKeyRepeatTimer() { + mInKeyRepeat = false; + removeMessages(MSG_REPEAT_KEY); + } + + public boolean isInKeyRepeat() { + return mInKeyRepeat; + } + + public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) { + cancelLongPressTimers(); + sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay); + } + + public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) { + cancelLongPressTimers(); + if (ENABLE_CAPSLOCK_BY_LONGPRESS) { + sendMessageDelayed( + obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay); + } + } + + public void cancelLongPressTimers() { + removeMessages(MSG_LONGPRESS_KEY); + removeMessages(MSG_LONGPRESS_SHIFT_KEY); + } + + public void cancelKeyTimers() { + cancelKeyRepeatTimer(); + cancelLongPressTimers(); + removeMessages(MSG_IGNORE_DOUBLE_TAP); + } + + public void startIgnoringDoubleTap() { + sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP), + ViewConfiguration.getDoubleTapTimeout()); + } + + public boolean isIgnoringDoubleTap() { + return hasMessages(MSG_IGNORE_DOUBLE_TAP); + } + + public void cancelAllMessages() { + cancelKeyTimers(); + } + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.keyboardViewStyle); + } + + public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); + mKeyHysteresisDistance = a.getDimensionPixelOffset( + R.styleable.KeyboardView_keyHysteresisDistance, 0); + mVerticalCorrection = a.getDimensionPixelOffset( + R.styleable.KeyboardView_verticalCorrection, 0); + + mPopupLayout = a.getResourceId(R.styleable.KeyboardView_popupLayout, 0); + // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount) + a.recycle(); + + final Resources res = getResources(); + + mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density); + // TODO: Refer to frameworks/base/core/res/res/values/config.xml + mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation); + + GestureDetector.SimpleOnGestureListener listener = + new GestureDetector.SimpleOnGestureListener() { + private boolean mProcessingShiftDoubleTapEvent = false; + + @Override + public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX, + float velocityY) { + final float absX = Math.abs(velocityX); + final float absY = Math.abs(velocityY); + float deltaY = me2.getY() - me1.getY(); + int travelY = getHeight() / 2; // Half the keyboard height + mSwipeTracker.computeCurrentVelocity(1000); + final float endingVelocityY = mSwipeTracker.getYVelocity(); + if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) { + if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) { + onSwipeDown(); + return true; + } + } + return false; + } + + @Override + public boolean onDoubleTap(MotionEvent firstDown) { + final Keyboard keyboard = getKeyboard(); + if (ENABLE_CAPSLOCK_BY_DOUBLETAP && keyboard instanceof LatinKeyboard + && ((LatinKeyboard) keyboard).isAlphaKeyboard()) { + final int pointerIndex = firstDown.getActionIndex(); + final int id = firstDown.getPointerId(pointerIndex); + final PointerTracker tracker = getPointerTracker(id); + // If the first down event is on shift key. + if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) { + mProcessingShiftDoubleTapEvent = true; + return true; + } + } + mProcessingShiftDoubleTapEvent = false; + return false; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent secondTap) { + if (mProcessingShiftDoubleTapEvent + && secondTap.getAction() == MotionEvent.ACTION_DOWN) { + final MotionEvent secondDown = secondTap; + final int pointerIndex = secondDown.getActionIndex(); + final int id = secondDown.getPointerId(pointerIndex); + final PointerTracker tracker = getPointerTracker(id); + // If the second down event is also on shift key. + if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) { + // Detected a double tap on shift key. If we are in the ignoring double tap + // mode, it means we have already turned off caps lock in + // {@link KeyboardSwitcher#onReleaseShift} . + final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap(); + if (!ignoringDoubleTap) + onDoubleTapShiftKey(tracker); + return true; + } + // Otherwise these events should not be handled as double tap. + mProcessingShiftDoubleTapEvent = false; + } + return mProcessingShiftDoubleTapEvent; + } + }; + + final boolean ignoreMultitouch = true; + mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch); + mGestureDetector.setIsLongpressEnabled(false); + + mHasDistinctMultitouch = context.getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); + mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval); + } + + public void startIgnoringDoubleTap() { + if (ENABLE_CAPSLOCK_BY_DOUBLETAP) + mHandler.startIgnoringDoubleTap(); + } + + public void setOnKeyboardActionListener(KeyboardActionListener listener) { + mKeyboardActionListener = listener; + for (PointerTracker tracker : mPointerTrackers) { + tracker.setOnKeyboardActionListener(listener); + } + } + + /** + * Returns the {@link KeyboardActionListener} object. + * @return the listener attached to this keyboard + */ + protected KeyboardActionListener getOnKeyboardActionListener() { + return mKeyboardActionListener; + } + + /** + * Attaches a keyboard to this view. The keyboard can be switched at any time and the + * view will re-layout itself to accommodate the keyboard. + * @see Keyboard + * @see #getKeyboard() + * @param keyboard the keyboard to display in this view + */ + @Override + public void setKeyboard(Keyboard keyboard) { + if (getKeyboard() != null) { + dismissAllKeyPreviews(); + } + // Remove any pending messages, except dismissing preview + mHandler.cancelKeyTimers(); + super.setKeyboard(keyboard); + mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(), + -getPaddingTop() + mVerticalCorrection); + for (PointerTracker tracker : mPointerTrackers) { + tracker.setKeyboard(keyboard, mKeyHysteresisDistance); + } + mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth()); + mPopupPanelCache.clear(); + } + + /** + * Returns whether the device has distinct multi-touch panel. + * @return true if the device has distinct multi-touch panel. + */ + public boolean hasDistinctMultitouch() { + return mHasDistinctMultitouch; + } + + /** + * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key + * codes for adjacent keys. When disabled, only the primary key code will be + * reported. + * @param enabled whether or not the proximity correction is enabled + */ + public void setProximityCorrectionEnabled(boolean enabled) { + mKeyDetector.setProximityCorrectionEnabled(enabled); + } + + /** + * Returns true if proximity correction is enabled. + */ + public boolean isProximityCorrectionEnabled() { + return mKeyDetector.isProximityCorrectionEnabled(); + } + + // TODO: clean up this method. + private void dismissAllKeyPreviews() { + for (PointerTracker tracker : mPointerTrackers) { + tracker.setReleasedKeyGraphics(); + dismissKeyPreview(tracker); + } + } + + @Override + public void cancelAllMessages() { + mHandler.cancelAllMessages(); + super.cancelAllMessages(); + } + + private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) { + // Check if we have a popup layout specified first. + if (mPopupLayout == 0) { + return false; + } + + final Key parentKey = tracker.getKey(keyIndex); + if (parentKey == null) + return false; + boolean result = onLongPress(parentKey, tracker); + if (result) { + dismissAllKeyPreviews(); + tracker.onLongPressed(mPointerQueue); + } + return result; + } + + private void onLongPressShiftKey(PointerTracker tracker) { + tracker.onLongPressed(mPointerQueue); + mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); + } + + private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) { + // When shift key is double tapped, the first tap is correctly processed as usual tap. And + // the second tap is treated as this double tap event, so that we need not mark tracker + // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue. + mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0); + } + + // This default implementation returns a popup mini keyboard panel. + // A derived class may return a language switcher popup panel, for instance. + protected PopupPanel onCreatePopupPanel(Key parentKey) { + if (parentKey.mPopupCharacters == null) + return null; + + final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null); + if (container == null) + throw new NullPointerException(); + + final PopupMiniKeyboardView miniKeyboardView = + (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view); + miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() { + @Override + public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) { + mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y); + dismissMiniKeyboard(); + } + + @Override + public void onTextInput(CharSequence text) { + mKeyboardActionListener.onTextInput(text); + dismissMiniKeyboard(); + } + + @Override + public void onCancelInput() { + mKeyboardActionListener.onCancelInput(); + dismissMiniKeyboard(); + } + + @Override + public void onSwipeDown() { + // Nothing to do. + } + @Override + public void onPress(int primaryCode, boolean withSliding) { + mKeyboardActionListener.onPress(primaryCode, withSliding); + } + @Override + public void onRelease(int primaryCode, boolean withSliding) { + mKeyboardActionListener.onRelease(primaryCode, withSliding); + } + }); + + final Keyboard parentKeyboard = getKeyboard(); + final Keyboard miniKeyboard = new MiniKeyboardBuilder( + this, parentKeyboard.getPopupKeyboardResId(), parentKey, parentKeyboard).build(); + miniKeyboardView.setKeyboard(miniKeyboard); + + container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + return miniKeyboardView; + } + + @Override + protected boolean needsToDimKeyboard() { + return mPopupMiniKeyboardPanel != null; + } + + /** + * Called when a key is long pressed. By default this will open mini keyboard associated + * with this key. + * @param parentKey the key that was long pressed + * @param tracker the pointer tracker which pressed the parent key + * @return true if the long press is handled, false otherwise. Subclasses should call the + * method on the base class if the subclass doesn't wish to handle the call. + */ + protected boolean onLongPress(Key parentKey, PointerTracker tracker) { + PopupPanel popupPanel = mPopupPanelCache.get(parentKey); + if (popupPanel == null) { + popupPanel = onCreatePopupPanel(parentKey); + if (popupPanel == null) + return false; + mPopupPanelCache.put(parentKey, popupPanel); + } + if (mPopupWindow == null) { + mPopupWindow = new PopupWindow(getContext()); + mPopupWindow.setBackgroundDrawable(null); + mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation); + // Allow popup window to be drawn off the screen. + mPopupWindow.setClippingEnabled(false); + } + mPopupMiniKeyboardPanel = popupPanel; + popupPanel.showPanel(this, parentKey, tracker, mPopupWindow); + + invalidateAllKeys(); + return true; + } + + private PointerTracker getPointerTracker(final int id) { + final ArrayList<PointerTracker> pointers = mPointerTrackers; + final KeyboardActionListener listener = mKeyboardActionListener; + final Keyboard keyboard = getKeyboard(); + + // Create pointer trackers until we can get 'id+1'-th tracker, if needed. + for (int i = pointers.size(); i <= id; i++) { + final PointerTracker tracker = + new PointerTracker(i, this, mHandler, mKeyDetector, this); + if (keyboard != null) + tracker.setKeyboard(keyboard, mKeyHysteresisDistance); + if (listener != null) + tracker.setOnKeyboardActionListener(listener); + pointers.add(tracker); + } + + return pointers.get(id); + } + + public boolean isInSlidingKeyInput() { + if (mPopupMiniKeyboardPanel != null) { + return mPopupMiniKeyboardPanel.isInSlidingKeyInput(); + } else { + return mPointerQueue.isInSlidingKeyInput(); + } + } + + public int getPointerCount() { + return mOldPointerCount; + } + + @Override + public boolean onTouchEvent(MotionEvent me) { + final int action = me.getActionMasked(); + final int pointerCount = me.getPointerCount(); + final int oldPointerCount = mOldPointerCount; + mOldPointerCount = pointerCount; + + // TODO: cleanup this code into a multi-touch to single-touch event converter class? + // If the device does not have distinct multi-touch support panel, ignore all multi-touch + // events except a transition from/to single-touch. + if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) { + return true; + } + + // Track the last few movements to look for spurious swipes. + mSwipeTracker.addMovement(me); + + // Gesture detector must be enabled only when mini-keyboard is not on the screen. + if (mPopupMiniKeyboardPanel == null && mGestureDetector != null + && mGestureDetector.onTouchEvent(me)) { + dismissAllKeyPreviews(); + mHandler.cancelKeyTimers(); + return true; + } + + final long eventTime = me.getEventTime(); + final int index = me.getActionIndex(); + final int id = me.getPointerId(index); + final int x = (int)me.getX(index); + final int y = (int)me.getY(index); + + // Needs to be called after the gesture detector gets a turn, as it may have displayed the + // mini keyboard + if (mPopupMiniKeyboardPanel != null) { + return mPopupMiniKeyboardPanel.onTouchEvent(me); + } + + if (mHandler.isInKeyRepeat()) { + final PointerTracker tracker = getPointerTracker(id); + // Key repeating timer will be canceled if 2 or more keys are in action, and current + // event (UP or DOWN) is non-modifier key. + if (pointerCount > 1 && !tracker.isModifier()) { + mHandler.cancelKeyRepeatTimer(); + } + // Up event will pass through. + } + + // TODO: cleanup this code into a multi-touch to single-touch event converter class? + // Translate mutli-touch event to single-touch events on the device that has no distinct + // multi-touch panel. + if (!mHasDistinctMultitouch) { + // Use only main (id=0) pointer tracker. + PointerTracker tracker = getPointerTracker(0); + if (pointerCount == 1 && oldPointerCount == 2) { + // Multi-touch to single touch transition. + // Send a down event for the latest pointer if the key is different from the + // previous key. + final int newKeyIndex = tracker.getKeyIndexOn(x, y); + if (mOldKeyIndex != newKeyIndex) { + tracker.onDownEvent(x, y, eventTime, null); + if (action == MotionEvent.ACTION_UP) + tracker.onUpEvent(x, y, eventTime, null); + } + } else if (pointerCount == 2 && oldPointerCount == 1) { + // Single-touch to multi-touch transition. + // Send an up event for the last pointer. + final int lastX = tracker.getLastX(); + final int lastY = tracker.getLastY(); + mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY); + tracker.onUpEvent(lastX, lastY, eventTime, null); + } else if (pointerCount == 1 && oldPointerCount == 1) { + tracker.onTouchEvent(action, x, y, eventTime, null); + } else { + Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount + + " (old " + oldPointerCount + ")"); + } + return true; + } + + final PointerTrackerQueue queue = mPointerQueue; + if (action == MotionEvent.ACTION_MOVE) { + for (int i = 0; i < pointerCount; i++) { + final PointerTracker tracker = getPointerTracker(me.getPointerId(i)); + tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue); + } + } else { + final PointerTracker tracker = getPointerTracker(id); + switch (action) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + tracker.onDownEvent(x, y, eventTime, queue); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + tracker.onUpEvent(x, y, eventTime, queue); + break; + case MotionEvent.ACTION_CANCEL: + tracker.onCancelEvent(x, y, eventTime, queue); + break; + } + } + + return true; + } + + protected void onSwipeDown() { + mKeyboardActionListener.onSwipeDown(); + } + + @Override + public void closing() { + super.closing(); + dismissMiniKeyboard(); + mPopupPanelCache.clear(); + } + + private boolean dismissMiniKeyboard() { + if (mPopupWindow != null && mPopupWindow.isShowing()) { + mPopupWindow.dismiss(); + mPopupMiniKeyboardPanel = null; + invalidateAllKeys(); + return true; + } + return false; + } + + public boolean handleBack() { + return dismissMiniKeyboard(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event) + || super.dispatchTouchEvent(event); + } + + return super.dispatchTouchEvent(event); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + final PointerTracker tracker = getPointerTracker(0); + return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent( + event, tracker) || super.dispatchPopulateAccessibilityEvent(event); + } + + return super.dispatchPopulateAccessibilityEvent(event); + } + + public boolean onHoverEvent(MotionEvent event) { + // Since reflection doesn't support calling superclass methods, this + // method checks for the existence of onHoverEvent() in the View class + // before returning a value. + if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) { + final PointerTracker tracker = getPointerTracker(0); + return AccessibleKeyboardViewProxy.getInstance().onHoverEvent(event, tracker); + } + + return false; + } +} diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java index 75d022b52..510bc16b4 100644 --- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java @@ -27,7 +27,7 @@ import com.android.inputmethod.latin.LatinImeLogger; import com.android.inputmethod.latin.Utils; // TODO: We should remove this class -public class LatinKeyboardView extends KeyboardView { +public class LatinKeyboardView extends LatinKeyboardBaseView { private static final String TAG = LatinKeyboardView.class.getSimpleName(); private static boolean DEBUG_MODE = LatinImeLogger.sDBG; diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java index 6228cc8b2..68284cd1b 100644 --- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java +++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java @@ -21,7 +21,7 @@ import android.os.SystemClock; import android.util.Log; import android.view.MotionEvent; -import com.android.inputmethod.keyboard.KeyboardView.UIHandler; +import com.android.inputmethod.keyboard.LatinKeyboardBaseView.UIHandler; import com.android.inputmethod.keyboard.internal.PointerTrackerKeyState; import com.android.inputmethod.keyboard.internal.PointerTrackerQueue; import com.android.inputmethod.latin.LatinImeLogger; @@ -42,8 +42,8 @@ public class PointerTracker { public interface UIProxy { public void invalidateKey(Key key); public void showKeyPreview(int keyIndex, PointerTracker tracker); + public void cancelShowKeyPreview(PointerTracker tracker); public void dismissKeyPreview(PointerTracker tracker); - public boolean hasDistinctMultitouch(); } public final int mPointerId; @@ -53,7 +53,7 @@ public class PointerTracker { private final int mLongPressKeyTimeout; private final int mLongPressShiftKeyTimeout; - private final KeyboardView mKeyboardView; + private final LatinKeyboardBaseView mKeyboardView; private final UIProxy mProxy; private final UIHandler mHandler; private final KeyDetector mKeyDetector; @@ -112,7 +112,7 @@ public class PointerTracker { public void onSwipeDown() {} }; - public PointerTracker(int id, KeyboardView keyboardView, UIHandler handler, + public PointerTracker(int id, LatinKeyboardBaseView keyboardView, UIHandler handler, KeyDetector keyDetector, UIProxy proxy) { if (proxy == null || handler == null || keyDetector == null) throw new NullPointerException(); @@ -123,7 +123,7 @@ public class PointerTracker { mKeyDetector = keyDetector; mKeyboardSwitcher = KeyboardSwitcher.getInstance(); mKeyState = new PointerTrackerKeyState(keyDetector); - mHasDistinctMultitouch = proxy.hasDistinctMultitouch(); + mHasDistinctMultitouch = keyboardView.hasDistinctMultitouch(); final Resources res = mKeyboardView.getResources(); mConfigSlidingKeyInputEnabled = res.getBoolean(R.bool.config_sliding_key_input_enabled); mDelayBeforeKeyRepeatStart = res.getInteger(R.integer.config_delay_before_key_repeat_start); @@ -504,7 +504,7 @@ public class PointerTracker { private void onUpEventInternal(int x, int y, long eventTime, boolean updateReleasedKeyGraphics) { mHandler.cancelKeyTimers(); - mHandler.cancelShowKeyPreview(this); + mProxy.cancelShowKeyPreview(this); mIsInSlidingKeyInput = false; final PointerTrackerKeyState keyState = mKeyState; final int keyX, keyY; @@ -564,7 +564,7 @@ public class PointerTracker { private void onCancelEventInternal() { mHandler.cancelKeyTimers(); - mHandler.cancelShowKeyPreview(this); + mProxy.cancelShowKeyPreview(this); dismissKeyPreview(); setReleasedKeyGraphics(mKeyState.getKeyIndex()); mIsInSlidingKeyInput = false; diff --git a/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java b/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java index 3b8c36487..959427aad 100644 --- a/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/PopupMiniKeyboardView.java @@ -31,7 +31,7 @@ import com.android.inputmethod.latin.R; * A view that renders a virtual {@link MiniKeyboard}. It handles rendering of keys and detecting * key presses and touch movements. */ -public class PopupMiniKeyboardView extends KeyboardView implements PopupPanel { +public class PopupMiniKeyboardView extends LatinKeyboardBaseView implements PopupPanel { private final int[] mCoordinates = new int[2]; private final boolean mConfigShowMiniKeyboardAtTouchedPoint; diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java index 3e433361a..e35db8955 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParser.java @@ -236,10 +236,10 @@ public class KeyboardParser { R.styleable.Keyboard_Key_maxPopupKeyboardColumn, 5)); mKeyboard.mIconsSet.loadIcons(keyboardAttr); - mKeyboardTopPadding = keyboardAttr.getDimensionPixelSize( - R.styleable.Keyboard_keyboardTopPadding, 0); - mKeyboardBottomPadding = keyboardAttr.getDimensionPixelSize( - R.styleable.Keyboard_keyboardBottomPadding, 0); + mKeyboardTopPadding = getDimensionOrFraction(keyboardAttr, + R.styleable.Keyboard_keyboardTopPadding, height, 0); + mKeyboardBottomPadding = getDimensionOrFraction(keyboardAttr, + R.styleable.Keyboard_keyboardBottomPadding, height, 0); } finally { keyAttr.recycle(); keyboardAttr.recycle(); diff --git a/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java index 1e67eec70..1885ea14e 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java +++ b/java/src/com/android/inputmethod/keyboard/internal/MiniKeyboardBuilder.java @@ -23,7 +23,7 @@ import android.graphics.Rect; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; -import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.LatinKeyboardBaseView; import com.android.inputmethod.keyboard.MiniKeyboard; import com.android.inputmethod.latin.R; @@ -43,6 +43,7 @@ public class MiniKeyboardBuilder { public final int mNumColumns; public final int mLeftKeys; public final int mRightKeys; // includes default key. + public int mTopPadding; /** * The object holding mini keyboard layout parameters. @@ -53,6 +54,7 @@ public class MiniKeyboardBuilder { * @param rowHeight mini keyboard row height in pixel, including vertical gap. * @param coordXInParent coordinate x of the popup key in parent keyboard. * @param parentKeyboardWidth parent keyboard width in pixel. + * parent keyboard. */ public MiniKeyboardLayoutParams(int numKeys, int maxColumns, int keyWidth, int rowHeight, int coordXInParent, int parentKeyboardWidth) { @@ -170,7 +172,7 @@ public class MiniKeyboardBuilder { } public int getY(int row) { - return (mNumRows - 1 - row) * mRowHeight; + return (mNumRows - 1 - row) * mRowHeight + mTopPadding; } public int getRowFlags(int row) { @@ -183,9 +185,21 @@ public class MiniKeyboardBuilder { private boolean isTopRow(int rowCount) { return rowCount == mNumRows - 1; } + + public void setTopPadding (int topPadding) { + mTopPadding = topPadding; + } + + public int getKeyboardHeight() { + return mNumRows * mRowHeight + mTopPadding; + } + + public int getKeyboardWidth() { + return mNumColumns * mKeyWidth; + } } - public MiniKeyboardBuilder(KeyboardView view, int layoutTemplateResId, Key parentKey, + public MiniKeyboardBuilder(LatinKeyboardBaseView view, int layoutTemplateResId, Key parentKey, Keyboard parentKeyboard) { final Context context = view.getContext(); mRes = context.getResources(); @@ -200,15 +214,16 @@ public class MiniKeyboardBuilder { keyWidth, parentKeyboard.getRowHeight(), parentKey.mX + (parentKey.mWidth + parentKey.mGap) / 2 - keyWidth / 2, view.getMeasuredWidth()); + params.setTopPadding(keyboard.getVerticalGap()); mParams = params; keyboard.setRowHeight(params.mRowHeight); - keyboard.setHeight(params.mNumRows * params.mRowHeight); - keyboard.setMinWidth(params.mNumColumns * params.mKeyWidth); + keyboard.setHeight(params.getKeyboardHeight()); + keyboard.setMinWidth(params.getKeyboardWidth()); keyboard.setDefaultCoordX(params.getDefaultKeyCoordX() + params.mKeyWidth / 2); } - private static int getMaxKeyWidth(KeyboardView view, CharSequence[] popupCharacters, + private static int getMaxKeyWidth(LatinKeyboardBaseView view, CharSequence[] popupCharacters, int minKeyWidth) { Paint paint = null; Rect bounds = null; diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java index a5bfea0f8..7ec18b38a 100644 --- a/java/src/com/android/inputmethod/latin/CandidateView.java +++ b/java/src/com/android/inputmethod/latin/CandidateView.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.os.Message; import android.text.Spannable; import android.text.SpannableString; @@ -30,6 +31,7 @@ import android.text.TextUtils; import android.text.style.BackgroundColorSpan; import android.text.style.CharacterStyle; import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import android.util.AttributeSet; import android.view.Gravity; @@ -38,7 +40,6 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.TextView; @@ -57,24 +58,29 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo public void pickSuggestionManually(int index, CharSequence word); } + private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. private static final int MAX_SUGGESTIONS = 18; - private static final int MATCH_PARENT = MeasureSpec.makeMeasureSpec( - -1, MeasureSpec.UNSPECIFIED); + private static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; + private static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; private static final boolean DBG = LatinImeLogger.sDBG; - private final View mCandidatesStrip; - private static final int NUM_CANDIDATES_IN_STRIP = 3; - private final ImageView mExpandCandidatesPane; - private final ImageView mCloseCandidatesPane; + private final ViewGroup mCandidatesStrip; + private final int mCandidateCountInStrip; + private static final int DEFAULT_CANDIDATE_COUNT_IN_STRIP = 3; + private final ViewGroup mCandidatesPaneControl; + private final TextView mExpandCandidatesPane; + private final TextView mCloseCandidatesPane; private ViewGroup mCandidatesPane; private ViewGroup mCandidatesPaneContainer; private View mKeyboardView; + private final ArrayList<TextView> mWords = new ArrayList<TextView>(); private final ArrayList<TextView> mInfos = new ArrayList<TextView>(); private final ArrayList<View> mDividers = new ArrayList<View>(); + private final int mCandidateStripHeight; private final CharacterStyle mInvertedForegroundColorSpan; private final CharacterStyle mInvertedBackgroundColorSpan; @@ -85,6 +91,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo private final int mColorTypedWord; private final int mColorAutoCorrect; private final int mColorSuggestedCandidate; + private final PopupWindow mPreviewPopup; private final TextView mPreviewText; @@ -96,8 +103,9 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo private boolean mShowingAutoCorrectionInverted; private boolean mShowingAddToDictionary; - private static final float MIN_TEXT_XSCALE = 0.4f; - private static final String ELLIPSIS = "\u2026"; + private final CandidateViewLayoutParams mParams; + private static final int PUNCTUATIONS_IN_STRIP = 6; + private static final float MIN_TEXT_XSCALE = 0.75f; private final UiHandler mHandler = new UiHandler(this); @@ -150,6 +158,115 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } } + private static class CandidateViewLayoutParams { + public final TextPaint mPaint; + public final int mPadding; + public final int mDividerWidth; + public final int mDividerHeight; + public final int mControlWidth; + private final int mAutoCorrectHighlight; + + public final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(); + + public int mCountInStrip; + // True if the mCountInStrip suggestions can fit in suggestion strip in equally divided + // width without squeezing the text. + public boolean mCanUseFixedWidthColumns; + public int mMaxWidth; + public int mAvailableWidthForWords; + public int mConstantWidthForPaddings; + public int mVariableWidthForWords; + public float mScaleX; + + public CandidateViewLayoutParams(Resources res, TextView word, View divider, View control, + int autoCorrectHighlight) { + mPaint = new TextPaint(); + final float textSize = res.getDimension(R.dimen.candidate_text_size); + mPaint.setTextSize(textSize); + mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight(); + divider.measure(WRAP_CONTENT, MATCH_PARENT); + mDividerWidth = divider.getMeasuredWidth(); + mDividerHeight = divider.getMeasuredHeight(); + mControlWidth = control.getMeasuredWidth(); + mAutoCorrectHighlight = autoCorrectHighlight; + } + + public void layoutStrip(SuggestedWords suggestions, int maxWidth, int maxCount) { + final int size = suggestions.size(); + setupTexts(suggestions, size, mAutoCorrectHighlight); + mCountInStrip = Math.min(maxCount, size); + mScaleX = 1.0f; + + do { + mMaxWidth = maxWidth; + if (size > mCountInStrip) { + mMaxWidth -= mControlWidth; + } + + tryLayout(); + + if (mCanUseFixedWidthColumns) { + return; + } + if (mVariableWidthForWords <= mAvailableWidthForWords) { + return; + } + + final float scaleX = mAvailableWidthForWords / (float)mVariableWidthForWords; + if (scaleX >= MIN_TEXT_XSCALE) { + mScaleX = scaleX; + return; + } + + mCountInStrip--; + } while (mCountInStrip > 1); + } + + public void tryLayout() { + final int maxCount = mCountInStrip; + final int dividers = mDividerWidth * (maxCount - 1); + mConstantWidthForPaddings = dividers + mPadding * maxCount; + mAvailableWidthForWords = mMaxWidth - mConstantWidthForPaddings; + + mPaint.setTextScaleX(mScaleX); + final int maxFixedWidthForWord = (mMaxWidth - dividers) / maxCount - mPadding; + mCanUseFixedWidthColumns = true; + mVariableWidthForWords = 0; + for (int i = 0; i < maxCount; i++) { + final int width = getTextWidth(mTexts.get(i), mPaint); + if (width > maxFixedWidthForWord) + mCanUseFixedWidthColumns = false; + mVariableWidthForWords += width; + } + } + + private void setupTexts(SuggestedWords suggestions, int count, int autoCorrectHighlight) { + mTexts.clear(); + for (int i = 0; i < count; i++) { + final CharSequence suggestion = suggestions.getWord(i); + if (suggestion == null) continue; + + final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion + && ((i == 1 && !suggestions.mTypedWordValid) + || (i == 0 && suggestions.mTypedWordValid)); + // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 + // and there are multiple suggestions, such as the default punctuation list. + // TODO: Need to revisit this logic with bigram suggestions + final CharSequence styled = getStyledCandidateWord(suggestion, isAutoCorrect, + autoCorrectHighlight); + mTexts.add(styled); + } + } + + @Override + public String toString() { + return String.format( + "count=%d width=%d avail=%d fixcol=%s scaleX=%4.2f const=%d var=%d", + mCountInStrip, mMaxWidth, mAvailableWidthForWords, mCanUseFixedWidthColumns, + mScaleX, mConstantWidthForPaddings, mVariableWidthForWords); + } + } + /** * Construct a CandidateView for showing suggested words for completion. * @param context @@ -173,6 +290,16 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable( context, attrs, defStyle, R.style.CandidateViewStyle)); + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle); + mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0); + mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0); + mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0); + mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0); + mCandidateCountInStrip = a.getInt( + R.styleable.CandidateView_candidateCountInStrip, DEFAULT_CANDIDATE_COUNT_IN_STRIP); + a.recycle(); + Resources res = context.getResources(); LayoutInflater inflater = LayoutInflater.from(context); inflater.inflate(R.layout.candidates_strip, this); @@ -184,74 +311,57 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo mPreviewPopup.setContentView(mPreviewText); mPreviewPopup.setBackgroundDrawable(null); - mCandidatesStrip = findViewById(R.id.candidates_strip); + mCandidatesStrip = (ViewGroup)findViewById(R.id.candidates_strip); mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height); for (int i = 0; i < MAX_SUGGESTIONS; i++) { - final TextView word, info; - switch (i) { - case 0: - word = (TextView)findViewById(R.id.word_left); - info = (TextView)findViewById(R.id.info_left); - break; - case 1: - word = (TextView)findViewById(R.id.word_center); - info = (TextView)findViewById(R.id.info_center); - break; - case 2: - word = (TextView)findViewById(R.id.word_right); - info = (TextView)findViewById(R.id.info_right); - break; - default: - word = (TextView)inflater.inflate(R.layout.candidate_word, null); - info = (TextView)inflater.inflate(R.layout.candidate_info, null); - break; - } + final TextView word = (TextView)inflater.inflate(R.layout.candidate_word, null); word.setTag(i); word.setOnClickListener(this); if (i == 0) word.setOnLongClickListener(this); mWords.add(word); - mInfos.add(info); - if (i > 0) { - final View divider = inflater.inflate(R.layout.candidate_divider, null); - divider.measure(MATCH_PARENT, MATCH_PARENT); - mDividers.add(divider); - } + mInfos.add((TextView)inflater.inflate(R.layout.candidate_info, null)); + mDividers.add(inflater.inflate(R.layout.candidate_divider, null)); } mTouchToSave = findViewById(R.id.touch_to_save); mWordToSave = (TextView)findViewById(R.id.word_to_save); mWordToSave.setOnClickListener(this); - final TypedArray a = context.obtainStyledAttributes( - attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle); - mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0); - mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0); - mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0); - mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0); mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff); mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord); - mExpandCandidatesPane = (ImageView)findViewById(R.id.expand_candidates_pane); - mExpandCandidatesPane.setImageDrawable( - a.getDrawable(R.styleable.CandidateView_iconExpandPane)); + final TypedArray keyboardViewAttr = context.obtainStyledAttributes( + attrs, R.styleable.KeyboardView, R.attr.keyboardViewStyle, R.style.KeyboardView); + final Drawable keyBackground = keyboardViewAttr.getDrawable( + R.styleable.KeyboardView_keyBackground); + final int keyTextColor = keyboardViewAttr.getColor( + R.styleable.KeyboardView_keyTextColor, 0xFF000000); + keyboardViewAttr.recycle(); + + mCandidatesPaneControl = (ViewGroup)findViewById(R.id.candidates_pane_control); + mExpandCandidatesPane = (TextView)findViewById(R.id.expand_candidates_pane); + mExpandCandidatesPane.setBackgroundDrawable(keyBackground); + mExpandCandidatesPane.setTextColor(keyTextColor); mExpandCandidatesPane.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { expandCandidatesPane(); } }); - mCloseCandidatesPane = (ImageView)findViewById(R.id.close_candidates_pane); - mCloseCandidatesPane.setImageDrawable( - a.getDrawable(R.styleable.CandidateView_iconClosePane)); + mCloseCandidatesPane = (TextView)findViewById(R.id.close_candidates_pane); + mCloseCandidatesPane.setBackgroundDrawable(keyBackground); + mCloseCandidatesPane.setTextColor(keyTextColor); mCloseCandidatesPane.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { closeCandidatesPane(); } }); + mCandidatesPaneControl.measure(WRAP_CONTENT, WRAP_CONTENT); - a.recycle(); + mParams = new CandidateViewLayoutParams(res, + mWords.get(0), mDividers.get(0), mCandidatesPaneControl, mAutoCorrectHighlight); } /** @@ -272,6 +382,7 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo if (suggestions == null) return; mSuggestions = suggestions; + mExpandCandidatesPane.setEnabled(false); if (mShowingAutoCorrectionInverted) { mHandler.postUpdateSuggestions(); } else { @@ -279,15 +390,14 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } } - private CharSequence getStyledCandidateWord(CharSequence word, TextView v, - boolean isAutoCorrect) { - v.setTypeface(Typeface.DEFAULT); + private static CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect, + int autoCorrectHighlight) { if (!isAutoCorrect) return word; final Spannable spannedWord = new SpannableString(word); - if ((mAutoCorrectHighlight & AUTO_CORRECT_BOLD) != 0) - v.setTypeface(Typeface.DEFAULT_BOLD); - if ((mAutoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0) + if ((autoCorrectHighlight & AUTO_CORRECT_BOLD) != 0) + spannedWord.setSpan(BOLD_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + if ((autoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0) spannedWord.setSpan(UNDERLINE_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); return spannedWord; } @@ -313,113 +423,164 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo private void updateSuggestions() { final SuggestedWords suggestions = mSuggestions; final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList; + final int paneWidth = getWidth(); + final CandidateViewLayoutParams params = mParams; clear(); - final int paneWidth = getWidth(); - final int dividerWidth = mDividers.get(0).getMeasuredWidth(); - final int dividerHeight = mDividers.get(0).getMeasuredHeight(); - int x = 0; - int y = 0; - int fromIndex = NUM_CANDIDATES_IN_STRIP; - final int count = Math.min(mWords.size(), suggestions.size()); closeCandidatesPane(); - mExpandCandidatesPane.setVisibility(count > NUM_CANDIDATES_IN_STRIP ? VISIBLE : GONE); + if (suggestions.size() == 0) + return; + + params.layoutStrip(suggestions, paneWidth, suggestions.isPunctuationSuggestions() + ? PUNCTUATIONS_IN_STRIP : mCandidateCountInStrip); + + final int count = Math.min(mWords.size(), suggestions.size()); + if (count <= params.mCountInStrip && !DBG) { + mCandidatesPaneControl.setVisibility(GONE); + } else { + mCandidatesPaneControl.setVisibility(VISIBLE); + mExpandCandidatesPane.setVisibility(VISIBLE); + mExpandCandidatesPane.setEnabled(true); + } + + final int countInStrip = params.mCountInStrip; + View centeringFrom = null, lastView = null; + int x = 0, y = 0, infoX = 0; for (int i = 0; i < count; i++) { - final CharSequence suggestion = suggestions.getWord(i); + final int pos; + if (i <= 1) { + final boolean willAutoCorrect = !suggestions.mTypedWordValid + && suggestions.mHasMinimalSuggestion; + pos = willAutoCorrect ? 1 - i : i; + } else { + pos = i; + } + final CharSequence suggestion = suggestions.getWord(pos); if (suggestion == null) continue; final SuggestedWordInfo suggestionInfo = (suggestedWordInfoList != null) - ? suggestedWordInfoList.get(i) : null; + ? suggestedWordInfoList.get(pos) : null; final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion - && ((i == 1 && !suggestions.mTypedWordValid) - || (i == 0 && suggestions.mTypedWordValid)); + && ((pos == 1 && !suggestions.mTypedWordValid) + || (pos == 0 && suggestions.mTypedWordValid)); // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 // and there are multiple suggestions, such as the default punctuation list. // TODO: Need to revisit this logic with bigram suggestions - final boolean isSuggestedCandidate = (i != 0); + final boolean isSuggestedCandidate = (pos != 0); final boolean isPunctuationSuggestions = (suggestion.length() == 1 && count > 1); - final TextView word = mWords.get(i); + final TextView word = mWords.get(pos); + final TextPaint paint = word.getPaint(); // TODO: Reorder candidates in strip as appropriate. The center candidate should hold // the word when space is typed (valid typed word or auto corrected word). word.setTextColor(getCandidateTextColor(isAutoCorrect, isSuggestedCandidate || isPunctuationSuggestions, suggestionInfo)); - final CharSequence text = getStyledCandidateWord(suggestion, word, isAutoCorrect); - if (i < NUM_CANDIDATES_IN_STRIP) { - final View parent = (View)word.getParent(); - final int width = parent.getWidth() - word.getPaddingLeft() - - word.getPaddingRight(); - setTextWithAutoScaleAndEllipsis(text, width, word); - } else { - setTextWithAutoScaleAndEllipsis(text, paneWidth, word); - } + final CharSequence styled = params.mTexts.get(pos); final TextView info; if (DBG && suggestionInfo != null && !TextUtils.isEmpty(suggestionInfo.getDebugString())) { info = mInfos.get(i); info.setText(suggestionInfo.getDebugString()); - info.setVisibility(View.VISIBLE); } else { info = null; } - if (i < NUM_CANDIDATES_IN_STRIP) { + final CharSequence text; + final float scaleX; + if (i < countInStrip) { + if (i == 0 && params.mCountInStrip == 1) { + text = getEllipsizedText(styled, params.mMaxWidth, paint); + scaleX = paint.getTextScaleX(); + } else { + text = styled; + scaleX = params.mScaleX; + } + word.setText(text); + word.setTextScaleX(scaleX); + if (i != 0) { + // Add divider if this isn't the left most suggestion in candidate strip. + mCandidatesStrip.addView(mDividers.get(i)); + } + mCandidatesStrip.addView(word); + if (params.mCanUseFixedWidthColumns) { + setLayoutWeight(word, 1.0f, mCandidateStripHeight); + } else { + final int width = getTextWidth(text, paint) + params.mPadding; + setLayoutWeight(word, width, mCandidateStripHeight); + } if (info != null) { - word.measure(MATCH_PARENT, MATCH_PARENT); - info.measure(MATCH_PARENT, MATCH_PARENT); - final int width = word.getMeasuredWidth(); - final int infoWidth = info.getMeasuredWidth(); - FrameLayoutCompatUtils.placeViewAt( - info, width - infoWidth, 0, infoWidth, info.getMeasuredHeight()); + mCandidatesPane.addView(info); + info.measure(WRAP_CONTENT, WRAP_CONTENT); + final int width = info.getMeasuredWidth(); + y = info.getMeasuredHeight(); + FrameLayoutCompatUtils.placeViewAt(info, infoX, 0, width, y); + infoX += width * 2; } } else { - word.measure(MATCH_PARENT, MATCH_PARENT); - final int width = word.getMeasuredWidth(); - final int height = word.getMeasuredHeight(); - // TODO: Handle overflow case. - if (dividerWidth + x + width >= paneWidth) { - centeringCandidates(fromIndex, i - 1, x, paneWidth); + paint.setTextScaleX(1.0f); + final int textWidth = getTextWidth(styled, paint); + int available = paneWidth - x - params.mPadding; + if (textWidth >= available) { + // Needs new row, centering previous row. + centeringCandidates(centeringFrom, lastView, x, paneWidth); x = 0; y += mCandidateStripHeight; - fromIndex = i; } if (x != 0) { - final View divider = mDividers.get(i - NUM_CANDIDATES_IN_STRIP); + // Add divider if this isn't the left most suggestion in current row. + final View divider = mDividers.get(i); mCandidatesPane.addView(divider); FrameLayoutCompatUtils.placeViewAt( - divider, x, y + (mCandidateStripHeight - dividerHeight) / 2, - dividerWidth, dividerHeight); - x += dividerWidth; + divider, x, y + (mCandidateStripHeight - params.mDividerHeight) / 2, + params.mDividerWidth, params.mDividerHeight); + x += params.mDividerWidth; } + available = paneWidth - x - params.mPadding; + text = getEllipsizedText(styled, available, paint); + scaleX = paint.getTextScaleX(); + word.setText(text); + word.setTextScaleX(scaleX); mCandidatesPane.addView(word); + lastView = word; + if (x == 0) centeringFrom = word; + word.measure(WRAP_CONTENT, + MeasureSpec.makeMeasureSpec(mCandidateStripHeight, MeasureSpec.EXACTLY)); + final int width = word.getMeasuredWidth(); + final int height = word.getMeasuredHeight(); FrameLayoutCompatUtils.placeViewAt( word, x, y + (mCandidateStripHeight - height) / 2, width, height); + x += width; if (info != null) { mCandidatesPane.addView(info); - info.measure(MATCH_PARENT, MATCH_PARENT); + lastView = info; + info.measure(WRAP_CONTENT, WRAP_CONTENT); final int infoWidth = info.getMeasuredWidth(); FrameLayoutCompatUtils.placeViewAt( - info, x + width - infoWidth, y, infoWidth, info.getMeasuredHeight()); + info, x - infoWidth, y, infoWidth, info.getMeasuredHeight()); } - x += width; } } if (x != 0) { // Centering last candidates row. - centeringCandidates(fromIndex, count - 1, x, paneWidth); + centeringCandidates(centeringFrom, lastView, x, paneWidth); } } - private void centeringCandidates(int from, int to, int width, int paneWidth) { - final ViewGroup pane = mCandidatesPane; - final int fromIndex = pane.indexOfChild(mWords.get(from)); - final int toIndex; - if (mInfos.get(to).getParent() != null) { - toIndex = pane.indexOfChild(mInfos.get(to)); - } else { - toIndex = pane.indexOfChild(mWords.get(to)); + private static void setLayoutWeight(View v, float weight, int height) { + final ViewGroup.LayoutParams lp = v.getLayoutParams(); + if (lp instanceof LinearLayout.LayoutParams) { + final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; + llp.weight = weight; + llp.width = 0; + llp.height = height; } + } + + private void centeringCandidates(View from, View to, int width, int paneWidth) { + final ViewGroup pane = mCandidatesPane; + final int fromIndex = pane.indexOfChild(from); + final int toIndex = pane.indexOfChild(to); final int offset = (paneWidth - width) / 2; for (int index = fromIndex; index <= toIndex; index++) { offsetMargin(pane.getChildAt(index), offset, 0); @@ -436,73 +597,68 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo } } - private static void setTextWithAutoScaleAndEllipsis(CharSequence text, int w, TextView v) { - // To prevent partially rendered character at the end of text, subtract few extra pixels - // from the width. - final int width = w - 4; - - final TextPaint paint = v.getPaint(); - final int textWidth = getTextWidth(text, paint, 1.0f); - if (textWidth < width || textWidth == 0 || width <= 0) { - v.setTextScaleX(1.0f); - v.setText(text); - return; - } - - final float scaleX = Math.min((float)width / textWidth, 1.0f); + private static CharSequence getEllipsizedText(CharSequence text, int maxWidth, + TextPaint paint) { + paint.setTextScaleX(1.0f); + final int width = getTextWidth(text, paint); + final float scaleX = Math.min(maxWidth / (float)width, 1.0f); if (scaleX >= MIN_TEXT_XSCALE) { - v.setTextScaleX(scaleX); - v.setText(text); - return; + paint.setTextScaleX(scaleX); + return text; } - final int truncatedWidth = width - getTextWidth(ELLIPSIS, paint, MIN_TEXT_XSCALE); - final CharSequence ellipsized = getTextEllipsizedAtStart(text, paint, truncatedWidth); - v.setTextScaleX(MIN_TEXT_XSCALE); - v.setText(ELLIPSIS); - v.append(ellipsized); + // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To get + // squeezed and ellipsezed text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). + final CharSequence ellipsized = TextUtils.ellipsize( + text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE); + paint.setTextScaleX(MIN_TEXT_XSCALE); + return ellipsized; } - private static int getTextWidth(CharSequence text, TextPaint paint, float scaleX) { + private static int getTextWidth(CharSequence text, TextPaint paint) { if (TextUtils.isEmpty(text)) return 0; + final Typeface savedTypeface = paint.getTypeface(); + paint.setTypeface(getTextTypeface(text)); final int len = text.length(); final float[] widths = new float[len]; - paint.setTextScaleX(scaleX); final int count = paint.getTextWidths(text, 0, len, widths); - float width = 0; + int width = 0; for (int i = 0; i < count; i++) { - width += widths[i]; + width += Math.round(widths[i] + 0.5f); } - return (int)Math.round(width + 0.5); + paint.setTypeface(savedTypeface); + return width; } - private static CharSequence getTextEllipsizedAtStart(CharSequence text, TextPaint paint, - int maxWidth) { - final int len = text.length(); - final float[] widths = new float[len]; - final int count = paint.getTextWidths(text, 0, len, widths); - float width = 0; - for (int i = count - 1; i >= 0; i--) { - width += widths[i]; - if (width > maxWidth) - return text.subSequence(i + 1, len); + private static Typeface getTextTypeface(CharSequence text) { + if (!(text instanceof SpannableString)) + return Typeface.DEFAULT; + + final SpannableString ss = (SpannableString)text; + final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); + if (styles.length == 0) + return Typeface.DEFAULT; + + switch (styles[0].getStyle()) { + case Typeface.BOLD: return Typeface.DEFAULT_BOLD; + // TODO: BOLD_ITALIC, ITALIC case? + default: return Typeface.DEFAULT; } - return text; } private void expandCandidatesPane() { - mExpandCandidatesPane.setVisibility(View.GONE); - mCloseCandidatesPane.setVisibility(View.VISIBLE); + mExpandCandidatesPane.setVisibility(GONE); + mCloseCandidatesPane.setVisibility(VISIBLE); mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight()); - mCandidatesPaneContainer.setVisibility(View.VISIBLE); - mKeyboardView.setVisibility(View.GONE); + mCandidatesPaneContainer.setVisibility(VISIBLE); + mKeyboardView.setVisibility(GONE); } private void closeCandidatesPane() { - mExpandCandidatesPane.setVisibility(View.VISIBLE); - mCloseCandidatesPane.setVisibility(View.GONE); - mCandidatesPaneContainer.setVisibility(View.GONE); - mKeyboardView.setVisibility(View.VISIBLE); + mExpandCandidatesPane.setVisibility(VISIBLE); + mCloseCandidatesPane.setVisibility(GONE); + mCandidatesPaneContainer.setVisibility(GONE); + mKeyboardView.setVisibility(VISIBLE); } public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) { @@ -526,8 +682,9 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo public void showAddToDictionaryHint(CharSequence word) { mWordToSave.setText(word); mShowingAddToDictionary = true; - mCandidatesStrip.setVisibility(View.GONE); - mTouchToSave.setVisibility(View.VISIBLE); + mCandidatesStrip.setVisibility(GONE); + mCandidatesPaneControl.setVisibility(GONE); + mTouchToSave.setVisibility(VISIBLE); } public boolean dismissAddToDictionaryHint() { @@ -543,12 +700,9 @@ public class CandidateView extends LinearLayout implements OnClickListener, OnLo public void clear() { mShowingAddToDictionary = false; mShowingAutoCorrectionInverted = false; - for (int i = 0; i < NUM_CANDIDATES_IN_STRIP; i++) { - mWords.get(i).setText(null); - mInfos.get(i).setVisibility(View.GONE); - } - mTouchToSave.setVisibility(View.GONE); - mCandidatesStrip.setVisibility(View.VISIBLE); + mTouchToSave.setVisibility(GONE); + mCandidatesStrip.setVisibility(VISIBLE); + mCandidatesStrip.removeAllViews(); mCandidatesPane.removeAllViews(); } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index a83aca0a2..4a813541d 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -66,7 +66,7 @@ import com.android.inputmethod.deprecated.recorrection.Recorrection; import com.android.inputmethod.keyboard.Keyboard; import com.android.inputmethod.keyboard.KeyboardActionListener; import com.android.inputmethod.keyboard.KeyboardSwitcher; -import com.android.inputmethod.keyboard.KeyboardView; +import com.android.inputmethod.keyboard.LatinKeyboardBaseView; import com.android.inputmethod.keyboard.LatinKeyboard; import com.android.inputmethod.keyboard.LatinKeyboardView; @@ -655,7 +655,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar @Override public void onWindowHidden() { super.onWindowHidden(); - KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); + LatinKeyboardBaseView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); } @@ -668,7 +668,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar mVoiceProxy.flushVoiceInputLogs(mConfigurationChanging); - KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); + LatinKeyboardBaseView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView != null) inputView.closing(); if (mAutoDictionary != null) mAutoDictionary.flushPendingWrites(); if (mUserBigramDictionary != null) mUserBigramDictionary.flushPendingWrites(); @@ -677,8 +677,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar @Override public void onFinishInputView(boolean finishingInput) { super.onFinishInputView(finishingInput); - KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); - if (inputView != null) inputView.cancelAllMessage(); + LatinKeyboardBaseView inputView = mKeyboardSwitcher.getKeyboardView(); + if (inputView != null) inputView.cancelAllMessages(); // Remove pending messages related to update suggestions mHandler.cancelUpdateSuggestions(); mHandler.cancelUpdateOldSuggestions(); @@ -831,8 +831,8 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar SuggestedWords.Builder builder = new SuggestedWords.Builder() .setApplicationSpecifiedCompletions(applicationSpecifiedCompletions) - .setTypedWordValid(true) - .setHasMinimalSuggestion(true); + .setTypedWordValid(false) + .setHasMinimalSuggestion(false); // When in fullscreen mode, show completions generated by the application setSuggestions(builder.build()); mBestWord = null; @@ -866,7 +866,7 @@ public class LatinIME extends InputMethodServiceCompatWrapper implements Keyboar @Override public void onComputeInsets(InputMethodService.Insets outInsets) { super.onComputeInsets(outInsets); - final KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); + final LatinKeyboardBaseView inputView = mKeyboardSwitcher.getKeyboardView(); if (inputView == null || mCandidateViewContainer == null) return; final int containerHeight = mCandidateViewContainer.getHeight(); diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index baad66d87..d6b989073 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -18,14 +18,14 @@ package com.android.inputmethod.latin; import com.android.inputmethod.compat.CompatUtils; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; -import com.android.inputmethod.compat.InputMethodServiceCompatWrapper; import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.compat.VibratorCompatWrapper; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; +import android.app.Fragment; import android.app.backup.BackupManager; -import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -40,7 +40,6 @@ import android.preference.PreferenceActivity; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.speech.SpeechRecognizer; -import android.text.AutoText; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; @@ -288,7 +287,7 @@ public class Settings extends PreferenceActivity builder.addWord(puncs.subSequence(i, i + 1)); } } - return builder.build(); + return builder.setIsPunctuationSuggestions().build(); } } @@ -321,10 +320,22 @@ public class Settings extends PreferenceActivity mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff)); } + public Activity getActivityInternal() { + Object thisObject = (Object) this; + if (thisObject instanceof Activity) { + return (Activity) thisObject; + } else if (thisObject instanceof Fragment) { + return ((Fragment) thisObject).getActivity(); + } else { + return null; + } + } + @Override - protected void onCreate(Bundle icicle) { + public void onCreate(Bundle icicle) { super.onCreate(icicle); final Resources res = getResources(); + final Context context = getActivityInternal(); addPreferencesFromResource(R.xml.prefs); mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES); @@ -340,7 +351,7 @@ public class Settings extends PreferenceActivity mVoiceModeOff = getString(R.string.voice_mode_off); mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) .equals(mVoiceModeOff)); - mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(this); + mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(context); mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD); mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS); @@ -348,7 +359,8 @@ public class Settings extends PreferenceActivity mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS); if (mDebugSettingsPreference != null) { final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN); - debugSettingsIntent.setClassName(getPackageName(), DebugSettings.class.getName()); + debugSettingsIntent.setClassName( + context.getPackageName(), DebugSettings.class.getName()); mDebugSettingsPreference.setIntent(debugSettingsIntent); } @@ -371,7 +383,7 @@ public class Settings extends PreferenceActivity generalSettings.removePreference(mVoicePreference); } - if (!VibratorCompatWrapper.getInstance(this).hasVibrator()) { + if (!VibratorCompatWrapper.getInstance(context).hasVibrator()) { generalSettings.removePreference(findPreference(PREF_VIBRATE_ON)); } @@ -427,22 +439,17 @@ public class Settings extends PreferenceActivity (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY); final Intent intent = dictionaryLink.getIntent(); - final int number = getPackageManager().queryIntentActivities(intent, 0).size(); + final int number = context.getPackageManager().queryIntentActivities(intent, 0).size(); if (0 >= number) { textCorrectionGroup.removePreference(dictionaryLink); } } @Override - protected void onResume() { + public void onResume() { super.onResume(); - int autoTextSize = AutoText.getSize(getListView()); - if (autoTextSize < 1) { - ((PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY)) - .removePreference(mQuickFixes); - } if (!VoiceProxy.VOICE_INSTALLED - || !SpeechRecognizer.isRecognitionAvailable(this)) { + || !SpeechRecognizer.isRecognitionAvailable(getActivityInternal())) { getPreferenceScreen().removePreference(mVoicePreference); } else { updateVoiceModeSummary(); @@ -453,7 +460,7 @@ public class Settings extends PreferenceActivity } @Override - protected void onDestroy() { + public void onDestroy() { getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener( this); super.onDestroy(); @@ -461,7 +468,7 @@ public class Settings extends PreferenceActivity @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { - (new BackupManager(this)).dataChanged(); + (new BackupManager(getActivityInternal())).dataChanged(); // If turning on voice input, show dialog if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) { if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff) @@ -488,8 +495,9 @@ public class Settings extends PreferenceActivity public boolean onPreferenceClick(Preference pref) { if (pref == mInputLanguageSelection) { startActivity(CompatUtils.getInputLanguageSelectionIntent( - Utils.getInputMethodId(InputMethodManagerCompatWrapper.getInstance(this), - getApplicationInfo().packageName), 0)); + Utils.getInputMethodId( + InputMethodManagerCompatWrapper.getInstance(getActivityInternal()), + getActivityInternal().getApplicationInfo().packageName), 0)); return true; } return false; @@ -515,7 +523,7 @@ public class Settings extends PreferenceActivity private void showVoiceConfirmation() { mOkClicked = false; - showDialog(VOICE_INPUT_CONFIRM_DIALOG); + getActivityInternal().showDialog(VOICE_INPUT_CONFIRM_DIALOG); // Make URL in the dialog message clickable if (mDialog != null) { TextView textView = (TextView) mDialog.findViewById(android.R.id.message); @@ -531,7 +539,6 @@ public class Settings extends PreferenceActivity [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]); } - @Override protected Dialog onCreateDialog(int id) { switch (id) { case VOICE_INPUT_CONFIRM_DIALOG: @@ -548,7 +555,7 @@ public class Settings extends PreferenceActivity updateVoicePreference(); } }; - AlertDialog.Builder builder = new AlertDialog.Builder(this) + AlertDialog.Builder builder = new AlertDialog.Builder(getActivityInternal()) .setTitle(R.string.voice_warning_title) .setPositiveButton(android.R.string.ok, listener) .setNegativeButton(android.R.string.cancel, listener); diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java new file mode 100644 index 000000000..434d5d5ea --- /dev/null +++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java @@ -0,0 +1,6 @@ +package com.android.inputmethod.latin; + +import android.os.Bundle; + +public class SettingsActivity extends Settings { +}
\ No newline at end of file diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java index 6ca12c0c5..8fc19ae87 100644 --- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java +++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java @@ -188,6 +188,8 @@ public class SubtypeSwitcher { // TODO: Update an icon for shortcut IME final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts = mImm.getShortcutInputMethodsAndSubtypes(); + mShortcutInputMethodInfo = null; + mShortcutSubtype = null; for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) { List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi); // TODO: Returns the first found IMI for now. Should handle all shortcuts as diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java index a8cdfc02e..84db17504 100644 --- a/java/src/com/android/inputmethod/latin/SuggestedWords.java +++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java @@ -24,15 +24,17 @@ import java.util.HashSet; import java.util.List; public class SuggestedWords { - public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, null); + public static final SuggestedWords EMPTY = new SuggestedWords(null, false, false, false, null); public final List<CharSequence> mWords; public final boolean mTypedWordValid; public final boolean mHasMinimalSuggestion; + public final boolean mIsPunctuationSuggestions; public final List<SuggestedWordInfo> mSuggestedWordInfoList; private SuggestedWords(List<CharSequence> words, boolean typedWordValid, - boolean hasMinimalSuggestion, List<SuggestedWordInfo> suggestedWordInfoList) { + boolean hasMinimalSuggestion, boolean isPunctuationSuggestions, + List<SuggestedWordInfo> suggestedWordInfoList) { if (words != null) { mWords = words; } else { @@ -40,6 +42,7 @@ public class SuggestedWords { } mTypedWordValid = typedWordValid; mHasMinimalSuggestion = hasMinimalSuggestion; + mIsPunctuationSuggestions = isPunctuationSuggestions; mSuggestedWordInfoList = suggestedWordInfoList; } @@ -59,10 +62,15 @@ public class SuggestedWords { return mHasMinimalSuggestion && ((size() > 1 && !mTypedWordValid) || mTypedWordValid); } + public boolean isPunctuationSuggestions() { + return mIsPunctuationSuggestions; + } + public static class Builder { private List<CharSequence> mWords = new ArrayList<CharSequence>(); private boolean mTypedWordValid; private boolean mHasMinimalSuggestion; + private boolean mIsPunctuationSuggestions; private List<SuggestedWordInfo> mSuggestedWordInfoList = new ArrayList<SuggestedWordInfo>(); @@ -118,6 +126,11 @@ public class SuggestedWords { return this; } + public Builder setIsPunctuationSuggestions() { + mIsPunctuationSuggestions = true; + return this; + } + // Should get rid of the first one (what the user typed previously) from suggestions // and replace it with what the user currently typed. public Builder addTypedWordAndPreviousSuggestions(CharSequence typedWord, @@ -143,7 +156,7 @@ public class SuggestedWords { public SuggestedWords build() { return new SuggestedWords(mWords, mTypedWordValid, mHasMinimalSuggestion, - mSuggestedWordInfoList); + mIsPunctuationSuggestions, mSuggestedWordInfoList); } public int size() { |