LatinIME/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java

925 lines
37 KiB
Java

/*
* 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.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.text.TextUtils;
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.ViewGroup;
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.deprecated.VoiceProxy;
import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
import com.android.inputmethod.latin.LatinIME;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Locale;
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 LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
SuddenJumpingTouchEventHandler.ProcessMotionEvent {
private static final String TAG = LatinKeyboardView.class.getSimpleName();
private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
/* Space key and its icons, drawables and colors. */
private Key mSpaceKey;
private Drawable mSpaceIcon;
private final boolean mIsSpacebarTriggeringPopupByLongPress;
private static final int SPACE_LED_LENGTH_PERCENT = 80;
private final boolean mAutoCorrectionSpacebarLedEnabled;
private final Drawable mAutoCorrectionSpacebarLedIcon;
private final float mSpacebarTextRatio;
private float mSpacebarTextSize;
private final int mSpacebarTextColor;
private final int mSpacebarTextShadowColor;
private final HashMap<Integer, BitmapDrawable> mSpacebarDrawableCache =
new HashMap<Integer, BitmapDrawable>();
private boolean mAutoCorrectionSpacebarLedOn;
private boolean mNeedsToDisplayLanguage;
private Locale mSpacebarLocale;
private float mSpacebarTextFadeFactor = 0.0f;
// Height in space key the language name will be drawn. (proportional to space key height)
public static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f;
// If the full language name needs to be smaller than this value to be drawn on space key,
// its short language name will be used instead.
private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f;
private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
// Timing constants
private final int mKeyRepeatInterval;
// TODO: Kill process when the usability study mode was changed.
private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
// Mini keyboard
private PopupWindow mMoreKeysWindow;
private MoreKeysPanel mMoreKeysPanel;
private int mMoreKeysPanelPointerTrackerId;
private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
new WeakHashMap<Key, MoreKeysPanel>();
/** Listener for {@link KeyboardActionListener}. */
private KeyboardActionListener mKeyboardActionListener;
private boolean mHasDistinctMultitouch;
private int mOldPointerCount = 1;
private Key mOldKey;
private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
protected KeyDetector mKeyDetector;
// To detect double tap.
protected GestureDetector mGestureDetector;
private final KeyTimerHandler mKeyTimerHandler = new KeyTimerHandler(this);
private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
implements TimerProxy {
private static final int MSG_REPEAT_KEY = 1;
private static final int MSG_LONGPRESS_KEY = 2;
private static final int MSG_IGNORE_DOUBLE_TAP = 3;
private static final int MSG_KEY_TYPED = 4;
private boolean mInKeyRepeat;
public KeyTimerHandler(LatinKeyboardView outerInstance) {
super(outerInstance);
}
@Override
public void handleMessage(Message msg) {
final LatinKeyboardView keyboardView = getOuterInstance();
final PointerTracker tracker = (PointerTracker) msg.obj;
switch (msg.what) {
case MSG_REPEAT_KEY:
tracker.onRepeatKey(tracker.getKey());
startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, tracker);
break;
case MSG_LONGPRESS_KEY:
keyboardView.openMiniKeyboardIfRequired(tracker.getKey(), tracker);
break;
}
}
@Override
public void startKeyRepeatTimer(long delay, PointerTracker tracker) {
mInKeyRepeat = true;
sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, tracker), delay);
}
public void cancelKeyRepeatTimer() {
mInKeyRepeat = false;
removeMessages(MSG_REPEAT_KEY);
}
public boolean isInKeyRepeat() {
return mInKeyRepeat;
}
@Override
public void startLongPressTimer(long delay, PointerTracker tracker) {
cancelLongPressTimer();
sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
}
@Override
public void cancelLongPressTimer() {
removeMessages(MSG_LONGPRESS_KEY);
}
@Override
public void startKeyTypedTimer(long delay) {
removeMessages(MSG_KEY_TYPED);
sendMessageDelayed(obtainMessage(MSG_KEY_TYPED), delay);
}
@Override
public boolean isTyping() {
return hasMessages(MSG_KEY_TYPED);
}
@Override
public void cancelKeyTimers() {
cancelKeyRepeatTimer();
cancelLongPressTimer();
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();
}
}
private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
private boolean mProcessingShiftDoubleTapEvent = false;
@Override
public boolean onDoubleTap(MotionEvent firstDown) {
final Keyboard keyboard = getKeyboard();
if (ENABLE_CAPSLOCK_BY_DOUBLETAP && keyboard.mId.isAlphabetKeyboard()) {
final int pointerIndex = firstDown.getActionIndex();
final int id = firstDown.getPointerId(pointerIndex);
final PointerTracker tracker = PointerTracker.getPointerTracker(
id, LatinKeyboardView.this);
final Key key = tracker.getKeyOn((int)firstDown.getX(), (int)firstDown.getY());
// If the first down event is on shift key.
if (key != null && key.isShift()) {
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 = PointerTracker.getPointerTracker(
id, LatinKeyboardView.this);
final Key key = tracker.getKeyOn((int)secondDown.getX(), (int)secondDown.getY());
// If the second down event is also on shift key.
if (key != null && key.isShift()) {
// 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} .
onDoubleTapShiftKey(mKeyTimerHandler.isIgnoringDoubleTap());
return true;
}
// Otherwise these events should not be handled as double tap.
mProcessingShiftDoubleTapEvent = false;
}
return mProcessingShiftDoubleTapEvent;
}
}
public LatinKeyboardView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.latinKeyboardViewStyle);
}
public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
final Resources res = getResources();
mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
R.bool.config_show_mini_keyboard_at_touched_point);
final float keyHysteresisDistance = res.getDimension(R.dimen.key_hysteresis_distance);
mKeyDetector = new KeyDetector(keyHysteresisDistance);
final boolean ignoreMultitouch = true;
mGestureDetector = new GestureDetector(
getContext(), new DoubleTapListener(), null, ignoreMultitouch);
mGestureDetector.setIsLongpressEnabled(false);
mHasDistinctMultitouch = context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
PointerTracker.init(mHasDistinctMultitouch, getContext());
final int longPressSpaceKeyTimeout =
res.getInteger(R.integer.config_long_press_space_key_timeout);
mIsSpacebarTriggeringPopupByLongPress = (longPressSpaceKeyTimeout > 0);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
mAutoCorrectionSpacebarLedIcon = a.getDrawable(
R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
1000, 1000, 1) / 1000.0f;
mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
mSpacebarTextShadowColor = a.getColor(
R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
a.recycle();
}
public void startIgnoringDoubleTap() {
if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
mKeyTimerHandler.startIgnoringDoubleTap();
}
public void setKeyboardActionListener(KeyboardActionListener listener) {
mKeyboardActionListener = listener;
PointerTracker.setKeyboardActionListener(listener);
}
/**
* Returns the {@link KeyboardActionListener} object.
* @return the listener attached to this keyboard
*/
@Override
public KeyboardActionListener getKeyboardActionListener() {
return mKeyboardActionListener;
}
@Override
public KeyDetector getKeyDetector() {
return mKeyDetector;
}
@Override
public DrawingProxy getDrawingProxy() {
return this;
}
@Override
public TimerProxy getTimerProxy() {
return mKeyTimerHandler;
}
/**
* 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) {
// Remove any pending messages, except dismissing preview
mKeyTimerHandler.cancelKeyTimers();
super.setKeyboard(keyboard);
mKeyDetector.setKeyboard(
keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
mKeyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth);
PointerTracker.setKeyDetector(mKeyDetector);
mTouchScreenRegulator.setKeyboard(keyboard);
mMoreKeysPanelCache.clear();
mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
mSpaceIcon = keyboard.mIconsSet.getIconByAttrId(R.styleable.Keyboard_iconSpaceKey);
final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
mSpacebarLocale = keyboard.mId.mLocale;
clearSpacebarDrawableCache();
}
/**
* Returns whether the device has distinct multi-touch panel.
* @return true if the device has distinct multi-touch panel.
*/
public boolean hasDistinctMultitouch() {
return mHasDistinctMultitouch;
}
public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
mHasDistinctMultitouch = hasDistinctMultitouch;
}
/**
* 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 cancelAllMessages() {
mKeyTimerHandler.cancelAllMessages();
super.cancelAllMessages();
}
private boolean openMiniKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
// Check if we have a popup layout specified first.
if (mMoreKeysLayout == 0) {
return false;
}
// Check if we are already displaying popup panel.
if (mMoreKeysPanel != null)
return false;
if (parentKey == null)
return false;
return onLongPress(parentKey, tracker);
}
private void onDoubleTapShiftKey(final boolean ignore) {
// 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 mPointerQueue.
if (ignore) {
invokeCustomRequest(LatinIME.CODE_HAPTIC_AND_AUDIO_FEEDBACK);
} else {
invokeCodeInput(Keyboard.CODE_CAPSLOCK);
}
}
// This default implementation returns a more keys panel.
protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
if (parentKey.mMoreKeys == null)
return null;
final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
if (container == null)
throw new NullPointerException();
final MiniKeyboardView miniKeyboardView =
(MiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
final Keyboard parentKeyboard = getKeyboard();
final Keyboard miniKeyboard = new MiniKeyboard.Builder(
this, parentKeyboard.mMoreKeysTemplate, parentKey, parentKeyboard).build();
miniKeyboardView.setKeyboard(miniKeyboard);
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
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) {
final int primaryCode = parentKey.mCode;
final Keyboard keyboard = getKeyboard();
if (primaryCode == Keyboard.CODE_DIGIT0 && keyboard.mId.isPhoneKeyboard()) {
tracker.onLongPressed();
// Long pressing on 0 in phone number keypad gives you a '+'.
invokeCodeInput(Keyboard.CODE_PLUS);
invokeReleaseKey(primaryCode);
return true;
}
if (primaryCode == Keyboard.CODE_SHIFT && keyboard.mId.isAlphabetKeyboard()) {
tracker.onLongPressed();
invokeCodeInput(Keyboard.CODE_CAPSLOCK);
invokeReleaseKey(primaryCode);
return true;
}
if (primaryCode == Keyboard.CODE_SPACE) {
// Long pressing the space key invokes IME switcher dialog.
if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
tracker.onLongPressed();
invokeReleaseKey(primaryCode);
return true;
}
}
return openMoreKeysPanel(parentKey, tracker);
}
private boolean invokeCustomRequest(int code) {
return mKeyboardActionListener.onCustomRequest(code);
}
private void invokeCodeInput(int primaryCode) {
mKeyboardActionListener.onCodeInput(primaryCode, null,
KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
}
private void invokeReleaseKey(int primaryCode) {
mKeyboardActionListener.onReleaseKey(primaryCode, false);
}
private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
if (moreKeysPanel == null) {
moreKeysPanel = onCreateMoreKeysPanel(parentKey);
if (moreKeysPanel == null)
return false;
mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
}
if (mMoreKeysWindow == null) {
mMoreKeysWindow = new PopupWindow(getContext());
mMoreKeysWindow.setBackgroundDrawable(null);
mMoreKeysWindow.setAnimationStyle(R.style.MiniKeyboardAnimation);
}
mMoreKeysPanel = moreKeysPanel;
mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
final Keyboard keyboard = getKeyboard();
moreKeysPanel.setShifted(keyboard.isShiftedOrShiftLocked());
final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
: parentKey.mX + parentKey.mWidth / 2;
final int pointY = parentKey.mY - keyboard.mVerticalGap;
moreKeysPanel.showMoreKeysPanel(
this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
dimEntireKeyboard(true);
return true;
}
public boolean isInSlidingKeyInput() {
if (mMoreKeysPanel != null) {
return true;
} else {
return PointerTracker.isAnyInSlidingKeyInput();
}
}
public int getPointerCount() {
return mOldPointerCount;
}
@Override
public boolean onTouchEvent(MotionEvent me) {
if (getKeyboard() == null) {
return false;
}
return mTouchScreenRegulator.onTouchEvent(me);
}
@Override
public boolean processMotionEvent(MotionEvent me) {
final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
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 (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
return true;
}
// Gesture detector must be enabled only when mini-keyboard is not on the screen.
if (mMoreKeysPanel == null && mGestureDetector != null
&& mGestureDetector.onTouchEvent(me)) {
PointerTracker.dismissAllKeyPreviews();
mKeyTimerHandler.cancelKeyTimers();
return true;
}
final long eventTime = me.getEventTime();
final int index = me.getActionIndex();
final int id = me.getPointerId(index);
final int x, y;
if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
x = mMoreKeysPanel.translateX((int)me.getX(index));
y = mMoreKeysPanel.translateY((int)me.getY(index));
} else {
x = (int)me.getX(index);
y = (int)me.getY(index);
}
if (ENABLE_USABILITY_STUDY_LOG) {
final String eventTag;
switch (action) {
case MotionEvent.ACTION_UP:
eventTag = "[Up]";
break;
case MotionEvent.ACTION_DOWN:
eventTag = "[Down]";
break;
case MotionEvent.ACTION_POINTER_UP:
eventTag = "[PointerUp]";
break;
case MotionEvent.ACTION_POINTER_DOWN:
eventTag = "[PointerDown]";
break;
case MotionEvent.ACTION_MOVE: // Skip this as being logged below
eventTag = "";
break;
default:
eventTag = "[Action" + action + "]";
break;
}
if (!TextUtils.isEmpty(eventTag)) {
UsabilityStudyLogUtils.getInstance().write(
eventTag + eventTime + "," + id + "," + x + "," + y + "\t\t");
}
}
if (mKeyTimerHandler.isInKeyRepeat()) {
final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
// 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()) {
mKeyTimerHandler.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 (nonDistinctMultitouch) {
// Use only main (id=0) pointer tracker.
final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
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 Key newKey = tracker.getKeyOn(x, y);
if (mOldKey != newKey) {
tracker.onDownEvent(x, y, eventTime, this);
if (action == MotionEvent.ACTION_UP)
tracker.onUpEvent(x, y, eventTime);
}
} 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();
mOldKey = tracker.getKeyOn(lastX, lastY);
tracker.onUpEvent(lastX, lastY, eventTime);
} else if (pointerCount == 1 && oldPointerCount == 1) {
tracker.processMotionEvent(action, x, y, eventTime, this);
} else {
Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
+ " (old " + oldPointerCount + ")");
}
return true;
}
if (action == MotionEvent.ACTION_MOVE) {
for (int i = 0; i < pointerCount; i++) {
final PointerTracker tracker = PointerTracker.getPointerTracker(
me.getPointerId(i), this);
final int px, py;
if (mMoreKeysPanel != null
&& tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
px = mMoreKeysPanel.translateX((int)me.getX(i));
py = mMoreKeysPanel.translateY((int)me.getY(i));
} else {
px = (int)me.getX(i);
py = (int)me.getY(i);
}
tracker.onMoveEvent(px, py, eventTime);
if (ENABLE_USABILITY_STUDY_LOG) {
UsabilityStudyLogUtils.getInstance().write("[Move]" + eventTime + ","
+ me.getPointerId(i) + "," + px + "," + py + "\t\t");
}
}
} else {
final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
tracker.processMotionEvent(action, x, y, eventTime, this);
}
return true;
}
@Override
public void closing() {
super.closing();
dismissMoreKeysPanel();
mMoreKeysPanelCache.clear();
}
@Override
public boolean dismissMoreKeysPanel() {
if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
mMoreKeysWindow.dismiss();
mMoreKeysPanel = null;
mMoreKeysPanelPointerTrackerId = -1;
dimEntireKeyboard(false);
return true;
}
return false;
}
public boolean handleBack() {
return dismissMoreKeysPanel();
}
@Override
public void draw(Canvas c) {
Utils.GCUtils.getInstance().reset();
boolean tryGC = true;
for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
try {
super.draw(c);
tryGC = false;
} catch (OutOfMemoryError e) {
tryGC = Utils.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
}
}
}
@Override
protected void onAttachedToWindow() {
// Token is available from here.
VoiceProxy.getInstance().onAttachedToWindow();
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent(
event) || super.dispatchPopulateAccessibilityEvent(event);
}
return super.dispatchPopulateAccessibilityEvent(event);
}
/**
* Receives hover events from the input framework. This method overrides
* View.dispatchHoverEvent(MotionEvent) on SDK version ICS or higher. On
* lower SDK versions, this method is never called.
*
* @param event The motion event to be dispatched.
* @return {@code true} if the event was handled by the view, {@code false}
* otherwise
*/
public boolean dispatchHoverEvent(MotionEvent event) {
if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
}
// Reflection doesn't support calling superclass methods.
return false;
}
public void updateShortcutKey(boolean available) {
final Keyboard keyboard = getKeyboard();
if (keyboard == null) return;
final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
if (shortcutKey == null) return;
shortcutKey.setEnabled(available);
invalidateKey(shortcutKey);
}
public void updateSpacebar(float fadeFactor, boolean needsToDisplayLanguage) {
mSpacebarTextFadeFactor = fadeFactor;
mNeedsToDisplayLanguage = needsToDisplayLanguage;
updateSpacebarIcon();
invalidateKey(mSpaceKey);
}
public void updateAutoCorrectionState(boolean isAutoCorrection) {
if (!mAutoCorrectionSpacebarLedEnabled) return;
mAutoCorrectionSpacebarLedOn = isAutoCorrection;
updateSpacebarIcon();
invalidateKey(mSpaceKey);
}
@Override
protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
super.onDrawKeyTopVisuals(key, canvas, paint, params);
if (key.mCode == Keyboard.CODE_SPACE) {
// Whether space key needs to show the "..." popup hint for special purposes
if (mIsSpacebarTriggeringPopupByLongPress
&& Utils.hasMultipleEnabledIMEsOrSubtypes(true /* include aux subtypes */)) {
super.drawKeyPopupHint(key, canvas, paint, params);
}
}
}
// TODO: Get rid of this method and draw spacebar locale and auto correction spacebar LED
// in onDrawKeyTopVisuals.
private void updateSpacebarIcon() {
if (mSpaceKey == null) return;
if (mNeedsToDisplayLanguage) {
mSpaceKey.setIcon(getSpaceDrawable(mSpacebarLocale));
} else if (mAutoCorrectionSpacebarLedOn) {
mSpaceKey.setIcon(getSpaceDrawable(null));
} else {
mSpaceKey.setIcon(mSpaceIcon);
}
}
private static int getSpacebarTextColor(int color, float fadeFactor) {
final int newColor = Color.argb((int)(Color.alpha(color) * fadeFactor),
Color.red(color), Color.green(color), Color.blue(color));
return newColor;
}
// Compute width of text with specified text size using paint.
private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) {
paint.setTextSize(textSize);
paint.getTextBounds(text, 0, text.length(), bounds);
return bounds.width();
}
// Layout locale language name on spacebar.
private static String layoutSpacebar(Paint paint, Locale locale, int width,
float origTextSize) {
final Rect bounds = new Rect();
// Estimate appropriate language name text size to fit in maxTextWidth.
String language = Utils.getFullDisplayName(locale, true);
int textWidth = getTextWidth(paint, language, origTextSize, bounds);
// Assuming text width and text size are proportional to each other.
float textSize = origTextSize * Math.min(width / textWidth, 1.0f);
// allow variable text size
textWidth = getTextWidth(paint, language, textSize, bounds);
// If text size goes too small or text does not fit, use middle or short name
final boolean useMiddleName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
|| (textWidth > width);
final boolean useShortName;
if (useMiddleName) {
language = Utils.getMiddleDisplayLanguage(locale);
textWidth = getTextWidth(paint, language, origTextSize, bounds);
textSize = origTextSize * Math.min(width / textWidth, 1.0f);
useShortName = (textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME)
|| (textWidth > width);
} else {
useShortName = false;
}
if (useShortName) {
language = Utils.getShortDisplayLanguage(locale);
textWidth = getTextWidth(paint, language, origTextSize, bounds);
textSize = origTextSize * Math.min(width / textWidth, 1.0f);
}
paint.setTextSize(textSize);
return language;
}
private Integer getSpaceDrawableKey(Locale locale) {
return Arrays.hashCode(new Object[] {
locale,
mAutoCorrectionSpacebarLedOn,
mSpacebarTextFadeFactor
});
}
private void clearSpacebarDrawableCache() {
for (final BitmapDrawable drawable : mSpacebarDrawableCache.values()) {
final Bitmap bitmap = drawable.getBitmap();
bitmap.recycle();
}
mSpacebarDrawableCache.clear();
}
private BitmapDrawable getSpaceDrawable(Locale locale) {
final Integer hashCode = getSpaceDrawableKey(locale);
final BitmapDrawable cached = mSpacebarDrawableCache.get(hashCode);
if (cached != null) {
return cached;
}
final BitmapDrawable drawable = new BitmapDrawable(getResources(), drawSpacebar(
locale, mAutoCorrectionSpacebarLedOn, mSpacebarTextFadeFactor));
mSpacebarDrawableCache.put(hashCode, drawable);
return drawable;
}
private Bitmap drawSpacebar(Locale inputLocale, boolean isAutoCorrection,
float textFadeFactor) {
final int width = mSpaceKey.mWidth;
final int height = mSpaceIcon != null ? mSpaceIcon.getIntrinsicHeight() : mSpaceKey.mHeight;
final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(buffer);
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// If application locales are explicitly selected.
if (inputLocale != null) {
final Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setTextAlign(Align.CENTER);
final String language = layoutSpacebar(paint, inputLocale, width, mSpacebarTextSize);
// Draw language text with shadow
// In case there is no space icon, we will place the language text at the center of
// spacebar.
final float descent = paint.descent();
final float textHeight = -paint.ascent() + descent;
final float baseline = (mSpaceIcon != null) ? height * SPACEBAR_LANGUAGE_BASELINE
: height / 2 + textHeight / 2;
paint.setColor(getSpacebarTextColor(mSpacebarTextShadowColor, textFadeFactor));
canvas.drawText(language, width / 2, baseline - descent - 1, paint);
paint.setColor(getSpacebarTextColor(mSpacebarTextColor, textFadeFactor));
canvas.drawText(language, width / 2, baseline - descent, paint);
}
// Draw the spacebar icon at the bottom
if (isAutoCorrection) {
final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
int x = (width - iconWidth) / 2;
int y = height - iconHeight;
mAutoCorrectionSpacebarLedIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
mAutoCorrectionSpacebarLedIcon.draw(canvas);
} else if (mSpaceIcon != null) {
final int iconWidth = mSpaceIcon.getIntrinsicWidth();
final int iconHeight = mSpaceIcon.getIntrinsicHeight();
int x = (width - iconWidth) / 2;
int y = height - iconHeight;
mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight);
mSpaceIcon.draw(canvas);
}
return buffer;
}
}