diff options
Diffstat (limited to 'java/src/org/pocketworkstation/pckeyboard/LatinKeyboard.java')
-rw-r--r-- | java/src/org/pocketworkstation/pckeyboard/LatinKeyboard.java | 967 |
1 files changed, 967 insertions, 0 deletions
diff --git a/java/src/org/pocketworkstation/pckeyboard/LatinKeyboard.java b/java/src/org/pocketworkstation/pckeyboard/LatinKeyboard.java new file mode 100644 index 0000000..7cd7475 --- /dev/null +++ b/java/src/org/pocketworkstation/pckeyboard/LatinKeyboard.java @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2008 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 org.pocketworkstation.pckeyboard; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.TextPaint; +import android.util.Log; +import android.view.ViewConfiguration; +import android.view.inputmethod.EditorInfo; + +import java.util.List; +import java.util.Locale; + +public class LatinKeyboard extends Keyboard { + + private static final boolean DEBUG_PREFERRED_LETTER = true; + private static final String TAG = "PCKeyboardLK"; + private static final int OPACITY_FULLY_OPAQUE = 255; + private static final int SPACE_LED_LENGTH_PERCENT = 80; + + private Drawable mShiftLockIcon; + private Drawable mShiftLockPreviewIcon; + private Drawable mOldShiftIcon; + private Drawable mSpaceIcon; + private Drawable mSpaceAutoCompletionIndicator; + private Drawable mSpacePreviewIcon; + private Drawable mMicIcon; + private Drawable mMicPreviewIcon; + private Drawable mSettingsIcon; + private Drawable mSettingsPreviewIcon; + private Drawable m123MicIcon; + private Drawable m123MicPreviewIcon; + private final Drawable mButtonArrowLeftIcon; + private final Drawable mButtonArrowRightIcon; + private Key mShiftKey; + private Key mEnterKey; + private Key mF1Key; + private final Drawable mHintIcon; + private Key mSpaceKey; + private Key m123Key; + private final int[] mSpaceKeyIndexArray; + private int mSpaceDragStartX; + private int mSpaceDragLastDiff; + private Locale mLocale; + private LanguageSwitcher mLanguageSwitcher; + private final Resources mRes; + private final Context mContext; + private int mMode; + // Whether this keyboard has voice icon on it + private boolean mHasVoiceButton; + // Whether voice icon is enabled at all + private boolean mVoiceEnabled; + private final boolean mIsAlphaKeyboard; + private final boolean mIsAlphaFullKeyboard; + private final boolean mIsFnFullKeyboard; + private CharSequence m123Label; + private boolean mCurrentlyInSpace; + private SlidingLocaleDrawable mSlidingLocaleIcon; + private int[] mPrefLetterFrequencies; + private int mPrefLetter; + private int mPrefLetterX; + private int mPrefLetterY; + private int mPrefDistance; + + private int mExtensionResId; + + // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes + // non-private. + private final int mVerticalGap; + + private LatinKeyboard mExtensionKeyboard; + + private static final float SPACEBAR_DRAG_THRESHOLD = 0.51f; + private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; + private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f; + // Minimum width of space key preview (proportional to keyboard width) + private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f; + // Minimum width of space key preview (proportional to screen height) + private static final float SPACEBAR_POPUP_MAX_RATIO = 0.4f; + // Height in space key the language name will be drawn. (proportional to space key height) + private 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 static int sSpacebarVerticalCorrection; + + public LatinKeyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0, 0); + } + + public LatinKeyboard(Context context, int xmlLayoutResId, int mode, float kbHeightPercent) { + super(context, 0, xmlLayoutResId, mode, kbHeightPercent); + final Resources res = context.getResources(); + //Log.i("PCKeyboard", "keyHeight=" + this.getKeyHeight()); + //this.setKeyHeight(30); // is useless, see http://code.google.com/p/android/issues/detail?id=4532 + mContext = context; + mMode = mode; + mRes = res; + mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); + mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); + setDefaultBounds(mShiftLockPreviewIcon); + mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); + mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led); + mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space); + mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic); + mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic); + mSettingsIcon = res.getDrawable(R.drawable.sym_keyboard_settings); + mSettingsPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_settings); + setDefaultBounds(mMicPreviewIcon); + mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); + mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); + m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic); + m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic); + mHintIcon = res.getDrawable(R.drawable.hint_popup); + setDefaultBounds(m123MicPreviewIcon); + sSpacebarVerticalCorrection = res.getDimensionPixelOffset( + R.dimen.spacebar_vertical_correction); + mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty; + mIsAlphaFullKeyboard = xmlLayoutResId == R.xml.kbd_full; + mIsFnFullKeyboard = xmlLayoutResId == R.xml.kbd_full_fn; + // The index of space key is available only after Keyboard constructor has finished. + mSpaceKeyIndexArray = new int[] { indexOf(LatinIME.ASCII_SPACE) }; + // TODO remove this initialization after cleanup + mVerticalGap = super.getVerticalGap(); + } + + @Override + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + Key key = new LatinKey(res, parent, x, y, parser); + if (key.codes == null) return key; + switch (key.codes[0]) { + case LatinIME.ASCII_ENTER: + mEnterKey = key; + break; + case LatinKeyboardView.KEYCODE_F1: + mF1Key = key; + break; + case LatinIME.ASCII_SPACE: + mSpaceKey = key; + break; + case KEYCODE_MODE_CHANGE: + m123Key = key; + m123Label = key.label; + break; + } + + return key; + } + + void setImeOptions(Resources res, int mode, int options) { + mMode = mode; + // TODO should clean up this method + if (mEnterKey != null) { + // Reset some of the rarely used attributes. + mEnterKey.popupCharacters = null; + mEnterKey.popupResId = 0; + mEnterKey.text = null; + switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { + case EditorInfo.IME_ACTION_GO: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_go_key); + break; + case EditorInfo.IME_ACTION_NEXT: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_next_key); + break; + case EditorInfo.IME_ACTION_DONE: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_done_key); + break; + case EditorInfo.IME_ACTION_SEARCH: + mEnterKey.iconPreview = res.getDrawable( + R.drawable.sym_keyboard_feedback_search); + mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_search); + mEnterKey.label = null; + break; + case EditorInfo.IME_ACTION_SEND: + mEnterKey.iconPreview = null; + mEnterKey.icon = null; + mEnterKey.label = res.getText(R.string.label_send_key); + break; + default: + // Keep Return key in IM mode, we have a dedicated smiley key. + mEnterKey.iconPreview = res.getDrawable( + R.drawable.sym_keyboard_feedback_return); + mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_return); + mEnterKey.label = null; + break; + } + // Set the initial size of the preview icon + if (mEnterKey.iconPreview != null) { + setDefaultBounds(mEnterKey.iconPreview); + } + } + } + + void enableShiftLock() { + int index = getShiftKeyIndex(); + if (index >= 0) { + mShiftKey = getKeys().get(index); + mOldShiftIcon = mShiftKey.icon; + } + } + + @Override + public boolean setShiftState(int shiftState) { + if (mShiftKey != null) { + // Tri-state LED tracks "on" and "lock" states, icon shows Caps state. + mShiftKey.on = shiftState == SHIFT_ON || shiftState == SHIFT_LOCKED; + mShiftKey.locked = shiftState == SHIFT_LOCKED || shiftState == SHIFT_CAPS_LOCKED; + mShiftKey.icon = (shiftState == SHIFT_OFF || shiftState == SHIFT_ON || shiftState == SHIFT_LOCKED) ? + mOldShiftIcon : mShiftLockIcon; + return super.setShiftState(shiftState, false); + } else { + return super.setShiftState(shiftState, true); + } + } + + /* package */ boolean isAlphaKeyboard() { + return mIsAlphaKeyboard; + } + + public void setExtension(LatinKeyboard extKeyboard) { + mExtensionKeyboard = extKeyboard; + } + + public LatinKeyboard getExtension() { + return mExtensionKeyboard; + } + + public void updateSymbolIcons(boolean isAutoCompletion) { + updateDynamicKeys(); + if (mSpaceKey != null) { + updateSpaceBarForLocale(isAutoCompletion); + } + } + + private void setDefaultBounds(Drawable drawable) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + + public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) { + mHasVoiceButton = hasVoiceButton; + mVoiceEnabled = hasVoice; + updateDynamicKeys(); + } + + private void updateDynamicKeys() { + update123Key(); + updateF1Key(); + } + + private void update123Key() { + // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode. + if (m123Key != null && mIsAlphaKeyboard) { + if (mVoiceEnabled && !mHasVoiceButton) { + m123Key.icon = m123MicIcon; + m123Key.iconPreview = m123MicPreviewIcon; + m123Key.label = null; + } else { + m123Key.icon = null; + m123Key.iconPreview = null; + m123Key.label = m123Label; + } + } + } + + private void updateF1Key() { + // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key. + if (mF1Key == null) + return; + + if (mIsAlphaKeyboard) { + if (mMode == KeyboardSwitcher.MODE_URL) { + setNonMicF1Key(mF1Key, "/", R.xml.popup_slash); + } else if (mMode == KeyboardSwitcher.MODE_EMAIL) { + setNonMicF1Key(mF1Key, "@", R.xml.popup_at); + } else { + if (mVoiceEnabled && mHasVoiceButton) { + setMicF1Key(mF1Key); + } else { + setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); + } + } + } else if (mIsAlphaFullKeyboard) { + if (mVoiceEnabled && mHasVoiceButton) { + setMicF1Key(mF1Key); + } else { + setSettingsF1Key(mF1Key); + } + } else if (mIsFnFullKeyboard) { + setMicF1Key(mF1Key); + } else { // Symbols keyboard + if (mVoiceEnabled && mHasVoiceButton) { + setMicF1Key(mF1Key); + } else { + setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); + } + } + } + + private void setMicF1Key(Key key) { + // HACK: draw mMicIcon and mHintIcon at the same time + final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes, + drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon)); + + if (key.popupResId == 0) { + key.popupResId = R.xml.popup_mic; + } else { + key.modifier = true; + if (key.label != null) { + key.popupCharacters = (key.popupCharacters == null) ? + key.label : key.label + key.popupCharacters.toString(); + } + } + key.label = null; + key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE }; + key.icon = micWithSettingsHintDrawable; + key.iconPreview = mMicPreviewIcon; + } + + private void setSettingsF1Key(Key key) { + if (key.shiftLabel != null && key.label != null) { + key.codes = new int[] { key.label.charAt(0) }; + return; // leave key otherwise unmodified + } + final Drawable settingsHintDrawable = new BitmapDrawable(mRes, + drawSynthesizedSettingsHintImage(key.width, key.height, mSettingsIcon, mHintIcon)); + key.label = null; + key.icon = settingsHintDrawable; + key.codes = new int[] { LatinKeyboardView.KEYCODE_OPTIONS }; + key.popupResId = R.xml.popup_mic; + key.iconPreview = mSettingsPreviewIcon; + } + + private void setNonMicF1Key(Key key, String label, int popupResId) { + if (key.shiftLabel != null) { + key.codes = new int[] { key.label.charAt(0) }; + return; // leave key unmodified + } + key.label = label; + key.codes = new int[] { label.charAt(0) }; + key.popupResId = popupResId; + key.icon = mHintIcon; + key.iconPreview = null; + } + + public boolean isF1Key(Key key) { + return key == mF1Key; + } + + public static boolean hasPuncOrSmileysPopup(Key key) { + return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys; + } + + /** + * @return a key which should be invalidated. + */ + public Key onAutoCompletionStateChanged(boolean isAutoCompletion) { + updateSpaceBarForLocale(isAutoCompletion); + return mSpaceKey; + } + + public boolean isLanguageSwitchEnabled() { + return mLocale != null; + } + + private void updateSpaceBarForLocale(boolean isAutoCompletion) { + // If application locales are explicitly selected. + if (mLocale != null) { + mSpaceKey.icon = new BitmapDrawable(mRes, + drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion)); + } else { + // sym_keyboard_space_led can be shared with Black and White symbol themes. + if (isAutoCompletion) { + mSpaceKey.icon = new BitmapDrawable(mRes, + drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion)); + } else { + mSpaceKey.icon = mRes.getDrawable(R.drawable.sym_keyboard_space); + } + } + } + + // 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(); + } + + // Overlay two images: mainIcon and hintIcon. + private Bitmap drawSynthesizedSettingsHintImage( + int width, int height, Drawable mainIcon, Drawable hintIcon) { + if (mainIcon == null || hintIcon == null) + return null; + Rect hintIconPadding = new Rect(0, 0, 0, 0); + hintIcon.getPadding(hintIconPadding); + final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(buffer); + canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); + + // Draw main icon at the center of the key visual + // Assuming the hintIcon shares the same padding with the key's background drawable + final int drawableX = (width + hintIconPadding.left - hintIconPadding.right + - mainIcon.getIntrinsicWidth()) / 2; + final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom + - mainIcon.getIntrinsicHeight()) / 2; + setDefaultBounds(mainIcon); + canvas.translate(drawableX, drawableY); + mainIcon.draw(canvas); + canvas.translate(-drawableX, -drawableY); + + // Draw hint icon fully in the key + hintIcon.setBounds(0, 0, width, height); + hintIcon.draw(canvas); + return buffer; + } + + // Layout local language name and left and right arrow on space bar. + private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow, + Drawable rArrow, int width, int height, float origTextSize, + boolean allowVariableTextSize) { + final float arrowWidth = lArrow.getIntrinsicWidth(); + final float arrowHeight = lArrow.getIntrinsicHeight(); + final float maxTextWidth = width - (arrowWidth + arrowWidth); + final Rect bounds = new Rect(); + + // Estimate appropriate language name text size to fit in maxTextWidth. + String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); + int textWidth = getTextWidth(paint, language, origTextSize, bounds); + // Assuming text width and text size are proportional to each other. + float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); + + final boolean useShortName; + if (allowVariableTextSize) { + textWidth = getTextWidth(paint, language, textSize, bounds); + // If text size goes too small or text does not fit, use short name + useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME + || textWidth > maxTextWidth; + } else { + useShortName = textWidth > maxTextWidth; + textSize = origTextSize; + } + if (useShortName) { + language = LanguageSwitcher.toTitleCase(locale.getLanguage()); + textWidth = getTextWidth(paint, language, origTextSize, bounds); + textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); + } + paint.setTextSize(textSize); + + // Place left and right arrow just before and after language text. + final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; + final int top = (int)(baseline - arrowHeight); + final float remains = (width - textWidth) / 2; + lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline); + rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth), + (int)baseline); + + return language; + } + + private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion) { + final int width = mSpaceKey.width; + final int height = mSpaceIcon.getIntrinsicHeight(); + final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(buffer); + canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); + + // If application locales are explicitly selected. + if (mLocale != null) { + final Paint paint = new Paint(); + paint.setAlpha(opacity); + paint.setAntiAlias(true); + paint.setTextAlign(Align.CENTER); + + final boolean allowVariableTextSize = true; + Locale locale = mLanguageSwitcher.getInputLocale(); + //Log.i("PCKeyboard", "input locale: " + locale); + final String language = layoutSpaceBar(paint, locale, + mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height, + getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14), + allowVariableTextSize); + + // Draw language text with shadow + final int shadowColor = mRes.getColor(R.color.latinkeyboard_bar_language_shadow_white); + final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; + final float descent = paint.descent(); + paint.setColor(shadowColor); + canvas.drawText(language, width / 2, baseline - descent - 1, paint); + paint.setColor(mRes.getColor(R.color.latinkeyboard_dim_color_white)); + + canvas.drawText(language, width / 2, baseline - descent, paint); + + // Put arrows that are already layed out on either side of the text + if (mLanguageSwitcher.getLocaleCount() > 1) { + mButtonArrowLeftIcon.draw(canvas); + mButtonArrowRightIcon.draw(canvas); + } + } + + // Draw the spacebar icon at the bottom + if (isAutoCompletion) { + final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; + final int iconHeight = mSpaceAutoCompletionIndicator.getIntrinsicHeight(); + int x = (width - iconWidth) / 2; + int y = height - iconHeight; + mSpaceAutoCompletionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight); + mSpaceAutoCompletionIndicator.draw(canvas); + } else { + 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; + } + + private int getSpacePreviewWidth() { + final int width = Math.min( + Math.max(mSpaceKey.width, (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO)), + (int)(getScreenHeight() * SPACEBAR_POPUP_MAX_RATIO)); + return width; + } + + private void updateLocaleDrag(int diff) { + if (mSlidingLocaleIcon == null) { + final int width = getSpacePreviewWidth(); + final int height = mSpacePreviewIcon.getIntrinsicHeight(); + mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, width, height); + mSlidingLocaleIcon.setBounds(0, 0, width, height); + mSpaceKey.iconPreview = mSlidingLocaleIcon; + } + mSlidingLocaleIcon.setDiff(diff); + if (Math.abs(diff) == Integer.MAX_VALUE) { + mSpaceKey.iconPreview = mSpacePreviewIcon; + } else { + mSpaceKey.iconPreview = mSlidingLocaleIcon; + } + mSpaceKey.iconPreview.invalidateSelf(); + } + + public int getLanguageChangeDirection() { + if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2 + || Math.abs(mSpaceDragLastDiff) < getSpacePreviewWidth() * SPACEBAR_DRAG_THRESHOLD) { + return 0; // No change + } + return mSpaceDragLastDiff > 0 ? 1 : -1; + } + + public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion) { + mLanguageSwitcher = switcher; + Locale locale = mLanguageSwitcher.getLocaleCount() > 0 + ? mLanguageSwitcher.getInputLocale() + : null; + // If the language count is 1 and is the same as the system language, don't show it. + if (locale != null + && mLanguageSwitcher.getLocaleCount() == 1 + && mLanguageSwitcher.getSystemLocale().getLanguage() + .equalsIgnoreCase(locale.getLanguage())) { + locale = null; + } + mLocale = locale; + updateSymbolIcons(isAutoCompletion); + } + + boolean isCurrentlyInSpace() { + return mCurrentlyInSpace; + } + + void setPreferredLetters(int[] frequencies) { + mPrefLetterFrequencies = frequencies; + mPrefLetter = 0; + } + + void keyReleased() { + mCurrentlyInSpace = false; + mSpaceDragLastDiff = 0; + mPrefLetter = 0; + mPrefLetterX = 0; + mPrefLetterY = 0; + mPrefDistance = Integer.MAX_VALUE; + if (mSpaceKey != null) { + updateLocaleDrag(Integer.MAX_VALUE); + } + } + + /** + * Does the magic of locking the touch gesture into the spacebar when + * switching input languages. + */ + boolean isInside(LatinKey key, int x, int y) { + final int code = key.codes[0]; + if (code == KEYCODE_SHIFT || + code == KEYCODE_DELETE) { + // Adjust target area for these keys + y -= key.height / 10; + if (code == KEYCODE_SHIFT) { + if (key.x == 0) { + x += key.width / 6; // left shift + } else { + x -= key.width / 6; // right shift + } + } + if (code == KEYCODE_DELETE) x -= key.width / 6; + } else if (code == LatinIME.ASCII_SPACE) { + y += LatinKeyboard.sSpacebarVerticalCorrection; + if (mLanguageSwitcher.getLocaleCount() > 1) { + if (mCurrentlyInSpace) { + int diff = x - mSpaceDragStartX; + if (Math.abs(diff - mSpaceDragLastDiff) > 0) { + updateLocaleDrag(diff); + } + mSpaceDragLastDiff = diff; + return true; + } else { + boolean insideSpace = key.isInsideSuper(x, y); + if (insideSpace) { + mCurrentlyInSpace = true; + mSpaceDragStartX = x; + updateLocaleDrag(0); + } + return insideSpace; + } + } + } else if (mPrefLetterFrequencies != null) { + // New coordinate? Reset + if (mPrefLetterX != x || mPrefLetterY != y) { + mPrefLetter = 0; + mPrefDistance = Integer.MAX_VALUE; + } + // Handle preferred next letter + final int[] pref = mPrefLetterFrequencies; + if (mPrefLetter > 0) { + if (DEBUG_PREFERRED_LETTER) { + if (mPrefLetter == code && !key.isInsideSuper(x, y)) { + Log.d(TAG, "CORRECTED !!!!!!"); + } + } + return mPrefLetter == code; + } else { + final boolean inside = key.isInsideSuper(x, y); + int[] nearby = getNearestKeys(x, y); + List<Key> nearbyKeys = getKeys(); + if (inside) { + // If it's a preferred letter + if (inPrefList(code, pref)) { + // Check if its frequency is much lower than a nearby key + mPrefLetter = code; + mPrefLetterX = x; + mPrefLetterY = y; + for (int i = 0; i < nearby.length; i++) { + Key k = nearbyKeys.get(nearby[i]); + if (k != key && inPrefList(k.codes[0], pref)) { + final int dist = distanceFrom(k, x, y); + if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) && + (pref[k.codes[0]] > pref[mPrefLetter] * 3)) { + mPrefLetter = k.codes[0]; + mPrefDistance = dist; + if (DEBUG_PREFERRED_LETTER) { + Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!"); + } + break; + } + } + } + + return mPrefLetter == code; + } + } + + // Get the surrounding keys and intersect with the preferred list + // For all in the intersection + // if distance from touch point is within a reasonable distance + // make this the pref letter + // If no pref letter + // return inside; + // else return thiskey == prefletter; + + for (int i = 0; i < nearby.length; i++) { + Key k = nearbyKeys.get(nearby[i]); + if (inPrefList(k.codes[0], pref)) { + final int dist = distanceFrom(k, x, y); + if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB) + && dist < mPrefDistance) { + mPrefLetter = k.codes[0]; + mPrefLetterX = x; + mPrefLetterY = y; + mPrefDistance = dist; + } + } + } + // Didn't find any + if (mPrefLetter == 0) { + return inside; + } else { + return mPrefLetter == code; + } + } + } + + // Lock into the spacebar + if (mCurrentlyInSpace) return false; + + return key.isInsideSuper(x, y); + } + + private boolean inPrefList(int code, int[] pref) { + if (code < pref.length && code >= 0) return pref[code] > 0; + return false; + } + + private int distanceFrom(Key k, int x, int y) { + if (y > k.y && y < k.y + k.height) { + return Math.abs(k.x + k.width / 2 - x); + } else { + return Integer.MAX_VALUE; + } + } + + @Override + public int[] getNearestKeys(int x, int y) { + if (mCurrentlyInSpace) { + return mSpaceKeyIndexArray; + } else { + // Avoid dead pixels at edges of the keyboard + return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)), + Math.max(0, Math.min(y, getHeight() - 1))); + } + } + + private int indexOf(int code) { + List<Key> keys = getKeys(); + int count = keys.size(); + for (int i = 0; i < count; i++) { + if (keys.get(i).codes[0] == code) return i; + } + return -1; + } + + private int getTextSizeFromTheme(int style, int defValue) { + TypedArray array = mContext.getTheme().obtainStyledAttributes( + style, new int[] { android.R.attr.textSize }); + int resId = array.getResourceId(0, 0); + if (resId >= array.length()) { + Log.i(TAG, "getTextSizeFromTheme error: resId " + resId + " > " + array.length()); + return defValue; + } + int textSize = array.getDimensionPixelSize(resId, defValue); + return textSize; + } + + // TODO LatinKey could be static class + class LatinKey extends Key { + + // functional normal state (with properties) + private final int[] KEY_STATE_FUNCTIONAL_NORMAL = { + android.R.attr.state_single + }; + + // functional pressed state (with properties) + private final int[] KEY_STATE_FUNCTIONAL_PRESSED = { + android.R.attr.state_single, + android.R.attr.state_pressed + }; + + public LatinKey(Resources res, Keyboard.Row parent, int x, int y, + XmlResourceParser parser) { + super(res, parent, x, y, parser); + } + + // sticky is used for shift key. If a key is not sticky and is modifier, + // the key will be treated as functional. + private boolean isFunctionalKey() { + return !sticky && modifier; + } + + /** + * Overriding this method so that we can reduce the target area for certain keys. + */ + @Override + public boolean isInside(int x, int y) { + // TODO This should be done by parent.isInside(this, x, y) + // if Key.parent were protected. + boolean result = LatinKeyboard.this.isInside(this, x, y); + return result; + } + + boolean isInsideSuper(int x, int y) { + return super.isInside(x, y); + } + + @Override + public int[] getCurrentDrawableState() { + if (isFunctionalKey()) { + if (pressed) { + return KEY_STATE_FUNCTIONAL_PRESSED; + } else { + return KEY_STATE_FUNCTIONAL_NORMAL; + } + } + return super.getCurrentDrawableState(); + } + + @Override + public int squaredDistanceFrom(int x, int y) { + // We should count vertical gap between rows to calculate the center of this Key. + final int verticalGap = LatinKeyboard.this.mVerticalGap; + final int xDist = this.x + width / 2 - x; + final int yDist = this.y + (height + verticalGap) / 2 - y; + return xDist * xDist + yDist * yDist; + } + } + + /** + * Animation to be displayed on the spacebar preview popup when switching + * languages by swiping the spacebar. It draws the current, previous and + * next languages and moves them by the delta of touch movement on the spacebar. + */ + class SlidingLocaleDrawable extends Drawable { + + private final int mWidth; + private final int mHeight; + private final Drawable mBackground; + private final TextPaint mTextPaint; + private final int mMiddleX; + private final Drawable mLeftDrawable; + private final Drawable mRightDrawable; + private final int mThreshold; + private int mDiff; + private boolean mHitThreshold; + private String mCurrentLanguage; + private String mNextLanguage; + private String mPrevLanguage; + + public SlidingLocaleDrawable(Drawable background, int width, int height) { + mBackground = background; + setDefaultBounds(mBackground); + mWidth = width; + mHeight = height; + mTextPaint = new TextPaint(); + mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18)); + mTextPaint.setColor(R.color.latinkeyboard_transparent); + mTextPaint.setTextAlign(Align.CENTER); + mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE); + mTextPaint.setAntiAlias(true); + mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2; + mLeftDrawable = + mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left); + mRightDrawable = + mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right); + mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop(); + } + + private void setDiff(int diff) { + if (diff == Integer.MAX_VALUE) { + mHitThreshold = false; + mCurrentLanguage = null; + return; + } + mDiff = diff; + if (mDiff > mWidth) mDiff = mWidth; + if (mDiff < -mWidth) mDiff = -mWidth; + if (Math.abs(mDiff) > mThreshold) mHitThreshold = true; + invalidateSelf(); + } + + private String getLanguageName(Locale locale) { + return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + if (mHitThreshold) { + Paint paint = mTextPaint; + final int width = mWidth; + final int height = mHeight; + final int diff = mDiff; + final Drawable lArrow = mLeftDrawable; + final Drawable rArrow = mRightDrawable; + canvas.clipRect(0, 0, width, height); + if (mCurrentLanguage == null) { + final LanguageSwitcher languageSwitcher = mLanguageSwitcher; + mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale()); + mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale()); + mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale()); + } + // Draw language text with shadow + final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent(); + paint.setColor(mRes.getColor(R.color.latinkeyboard_feedback_language_text)); + canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint); + canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint); + canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint); + + setDefaultBounds(lArrow); + rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width, + rArrow.getIntrinsicHeight()); + lArrow.draw(canvas); + rArrow.draw(canvas); + } + if (mBackground != null) { + canvas.translate(mMiddleX, 0); + mBackground.draw(canvas); + } + canvas.restore(); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + // Ignore + } + + @Override + public void setColorFilter(ColorFilter cf) { + // Ignore + } + + @Override + public int getIntrinsicWidth() { + return mWidth; + } + + @Override + public int getIntrinsicHeight() { + return mHeight; + } + } +} |