Back to project page translationKeyboard.
The source code is released under:
GNU General Public License
If you think the Android project translationKeyboard listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2008-2009 Google Inc.// w ww .ja v a 2 s . c o m * * 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.distantshoresmedia.translationkeyboard; import org.distantshoresmedia.translationkeyboard.R; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.util.Xml; import android.util.DisplayMetrics; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.StringTokenizer; /** * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard * consists of rows of keys. * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> * <pre> * <Keyboard * android:keyWidth="%10p" * android:keyHeight="50px" * android:horizontalGap="2px" * android:verticalGap="2px" > * <Row android:keyWidth="32px" > * <Key android:keyLabel="A" /> * ... * </Row> * ... * </Keyboard> * </pre> * @attr ref android.R.styleable#Keyboard_keyWidth * @attr ref android.R.styleable#Keyboard_keyHeight * @attr ref android.R.styleable#Keyboard_horizontalGap * @attr ref android.R.styleable#Keyboard_verticalGap */ public class Keyboard { static final String TAG = "Keyboard"; public final static char DEAD_KEY_PLACEHOLDER = 0x25cc; // dotted small circle public final static String DEAD_KEY_PLACEHOLDER_STRING = Character.toString(DEAD_KEY_PLACEHOLDER); // Keyboard XML Tags private static final String TAG_KEYBOARD = "Keyboard"; private static final String TAG_ROW = "Row"; private static final String TAG_KEY = "Key"; public static final int EDGE_LEFT = 0x01; public static final int EDGE_RIGHT = 0x02; public static final int EDGE_TOP = 0x04; public static final int EDGE_BOTTOM = 0x08; public static final int KEYCODE_SHIFT = -1; public static final int KEYCODE_MODE_CHANGE = -2; public static final int KEYCODE_CANCEL = -3; public static final int KEYCODE_DONE = -4; public static final int KEYCODE_DELETE = -5; public static final int KEYCODE_ALT_SYM = -6; // Backwards compatible setting to avoid having to change all the kbd_qwerty files public static final int DEFAULT_LAYOUT_ROWS = 4; public static final int DEFAULT_LAYOUT_COLUMNS = 10; // Flag values for popup key contents. Keep in sync with strings.xml values. public static final int POPUP_ADD_SHIFT = 1; public static final int POPUP_ADD_CASE = 2; public static final int POPUP_ADD_SELF = 4; public static final int POPUP_DISABLE = 256; public static final int POPUP_AUTOREPEAT = 512; /** Horizontal gap default for all rows */ private float mDefaultHorizontalGap; private float mHorizontalPad; private float mVerticalPad; /** Default key width */ private float mDefaultWidth; /** Default key height */ private int mDefaultHeight; /** Default gap between rows */ private int mDefaultVerticalGap; public static final int SHIFT_OFF = 0; public static final int SHIFT_ON = 1; public static final int SHIFT_LOCKED = 2; public static final int SHIFT_CAPS = 3; public static final int SHIFT_CAPS_LOCKED = 4; /** Is the keyboard in the shifted state */ private int mShiftState = SHIFT_OFF; /** Key instance for the shift key, if present */ private Key mShiftKey; private Key mAltKey; private Key mCtrlKey; private Key mMetaKey; /** Key index for the shift key, if present */ private int mShiftKeyIndex = -1; /** Total height of the keyboard, including the padding and keys */ private int mTotalHeight; /** * Total width of the keyboard, including left side gaps and keys, but not any gaps on the * right side. */ private int mTotalWidth; /** List of keys in this keyboard */ private List<Key> mKeys; /** List of modifier keys such as Shift & Alt, if any */ private List<Key> mModifierKeys; /** Width of the screen available to fit the keyboard */ private int mDisplayWidth; /** Height of the screen and keyboard */ private int mDisplayHeight; private int mKeyboardHeight; /** Keyboard mode, or zero, if none. */ private int mKeyboardMode; private boolean mUseExtension; public int mLayoutRows; public int mLayoutColumns; public int mRowCount = 1; public int mExtensionRowCount = 0; // Variables for pre-computing nearest keys. private int mCellWidth; private int mCellHeight; private int[][] mGridNeighbors; private int mProximityThreshold; /** Number of key widths from current touch point to search for nearest keys. */ private static float SEARCH_DISTANCE = 1.8f; /** * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. * Some of the key size defaults can be overridden per row from what the {@link Keyboard} * defines. * @attr ref android.R.styleable#Keyboard_keyWidth * @attr ref android.R.styleable#Keyboard_keyHeight * @attr ref android.R.styleable#Keyboard_horizontalGap * @attr ref android.R.styleable#Keyboard_verticalGap * @attr ref android.R.styleable#Keyboard_Row_keyboardMode */ public static class Row { /** Default width of a key in this row. */ public float defaultWidth; /** Default height of a key in this row. */ public int defaultHeight; /** Default horizontal gap between keys in this row. */ public float defaultHorizontalGap; /** Vertical gap following this row. */ public int verticalGap; /** The keyboard mode for this row */ public int mode; public boolean extension; private Keyboard parent; public Row(Keyboard parent) { this.parent = parent; } public Row(Resources res, Keyboard parent, XmlResourceParser parser) { this.parent = parent; TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard); defaultWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth, parent.mDisplayWidth, parent.mDefaultWidth); defaultHeight = Math.round(getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight, parent.mDisplayHeight, parent.mDefaultHeight)); defaultHorizontalGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap, parent.mDisplayWidth, parent.mDefaultHorizontalGap); verticalGap = Math.round(getDimensionOrFraction(a, R.styleable.Keyboard_verticalGap, parent.mDisplayHeight, parent.mDefaultVerticalGap)); a.recycle(); a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Row); mode = a.getResourceId(R.styleable.Keyboard_Row_keyboardMode, 0); extension = a.getBoolean(R.styleable.Keyboard_Row_extension, false); if (parent.mLayoutRows >= 5) { boolean isTop = (extension || parent.mRowCount - parent.mExtensionRowCount <= 0); float topScale = LatinIME.sKeyboardSettings.topRowScale; float scale = isTop ? topScale : 1.0f + (1.0f - topScale) / (parent.mLayoutRows - 1); defaultHeight = Math.round(defaultHeight * scale); } a.recycle(); } } /** * Class for describing the position and characteristics of a single key in the keyboard. * * @attr ref android.R.styleable#Keyboard_keyWidth * @attr ref android.R.styleable#Keyboard_keyHeight * @attr ref android.R.styleable#Keyboard_horizontalGap * @attr ref android.R.styleable#Keyboard_Key_codes * @attr ref android.R.styleable#Keyboard_Key_keyIcon * @attr ref android.R.styleable#Keyboard_Key_keyLabel * @attr ref android.R.styleable#Keyboard_Key_iconPreview * @attr ref android.R.styleable#Keyboard_Key_isSticky * @attr ref android.R.styleable#Keyboard_Key_isRepeatable * @attr ref android.R.styleable#Keyboard_Key_isModifier * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard * @attr ref android.R.styleable#Keyboard_Key_popupCharacters * @attr ref android.R.styleable#Keyboard_Key_keyOutputText */ public static class Key { /** * All the key codes (unicode or custom code) that this key could generate, zero'th * being the most important. */ public int[] codes; /** Label to display */ public CharSequence label; public CharSequence shiftLabel; public CharSequence capsLabel; /** Icon to display instead of a label. Icon takes precedence over a label */ public Drawable icon; /** Preview version of the icon, for the preview popup */ public Drawable iconPreview; /** Width of the key, not including the gap */ public int width; /** Height of the key, not including the gap */ private float realWidth; public int height; /** The horizontal gap before this key */ public int gap; private float realGap; /** Whether this key is sticky, i.e., a toggle key */ public boolean sticky; /** X coordinate of the key in the keyboard layout */ public int x; private float realX; /** Y coordinate of the key in the keyboard layout */ public int y; /** The current pressed state of this key */ public boolean pressed; /** If this is a sticky key, is it on or locked? */ public boolean on; public boolean locked; /** Text to output when pressed. This can be multiple characters, like ".com" */ public CharSequence text; /** Popup characters */ public CharSequence popupCharacters; public boolean popupReversed; public boolean isCursor; public String hint; // Set by LatinKeyboardBaseView public String altHint; // Set by LatinKeyboardBaseView /** * Flags that specify the anchoring to edges of the keyboard for detecting touch events * that are just out of the boundary of the key. This is a bit mask of * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and * {@link Keyboard#EDGE_BOTTOM}. */ public int edgeFlags; /** Whether this is a modifier key, such as Shift or Alt */ public boolean modifier; /** The keyboard that this key belongs to */ private Keyboard keyboard; /** * If this key pops up a mini keyboard, this is the resource id for the XML layout for that * keyboard. */ public int popupResId; /** Whether this key repeats itself when held down */ public boolean repeatable; /** Is the shifted character the uppercase equivalent of the unshifted one? */ private boolean isSimpleUppercase; /** Is the shifted character a distinct uppercase char that's different from the shifted char? */ private boolean isDistinctUppercase; private final static int[] KEY_STATE_NORMAL_ON = { android.R.attr.state_checkable, android.R.attr.state_checked }; private final static int[] KEY_STATE_PRESSED_ON = { android.R.attr.state_pressed, android.R.attr.state_checkable, android.R.attr.state_checked }; private final static int[] KEY_STATE_NORMAL_LOCK = { android.R.attr.state_active, android.R.attr.state_checkable, android.R.attr.state_checked }; private final static int[] KEY_STATE_PRESSED_LOCK = { android.R.attr.state_active, android.R.attr.state_pressed, android.R.attr.state_checkable, android.R.attr.state_checked }; private final static int[] KEY_STATE_NORMAL_OFF = { android.R.attr.state_checkable }; private final static int[] KEY_STATE_PRESSED_OFF = { android.R.attr.state_pressed, android.R.attr.state_checkable }; private final static int[] KEY_STATE_NORMAL = { }; private final static int[] KEY_STATE_PRESSED = { android.R.attr.state_pressed }; /** Create an empty key with no attributes. */ public Key(Row parent) { keyboard = parent.parent; height = parent.defaultHeight; width = Math.round(parent.defaultWidth); realWidth = parent.defaultWidth; gap = Math.round(parent.defaultHorizontalGap); realGap = parent.defaultHorizontalGap; } /** Create a key with the given top-left coordinate and extract its attributes from * the XML parser. * @param res resources associated with the caller's context * @param parent the row that this key belongs to. The row must already be attached to * a {@link Keyboard}. * @param x the x coordinate of the top-left * @param y the y coordinate of the top-left * @param parser the XML parser containing the attributes for this key */ public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { this(parent); this.x = x; this.y = y; TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard); realWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth, keyboard.mDisplayWidth, parent.defaultWidth); float realHeight = getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight, keyboard.mDisplayHeight, parent.defaultHeight); realHeight -= parent.parent.mVerticalPad; height = Math.round(realHeight); this.y += parent.parent.mVerticalPad / 2; realGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap, keyboard.mDisplayWidth, parent.defaultHorizontalGap); realGap += parent.parent.mHorizontalPad; realWidth -= parent.parent.mHorizontalPad; width = Math.round(realWidth); gap = Math.round(realGap); a.recycle(); a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key); this.realX = this.x + realGap - parent.parent.mHorizontalPad / 2; this.x = Math.round(this.realX); TypedValue codesValue = new TypedValue(); a.getValue(R.styleable.Keyboard_Key_codes, codesValue); if (codesValue.type == TypedValue.TYPE_INT_DEC || codesValue.type == TypedValue.TYPE_INT_HEX) { codes = new int[] { codesValue.data }; } else if (codesValue.type == TypedValue.TYPE_STRING) { codes = parseCSV(codesValue.string.toString()); } iconPreview = a.getDrawable(R.styleable.Keyboard_Key_iconPreview); if (iconPreview != null) { iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), iconPreview.getIntrinsicHeight()); } popupCharacters = a.getText( R.styleable.Keyboard_Key_popupCharacters); popupResId = a.getResourceId( R.styleable.Keyboard_Key_popupKeyboard, 0); repeatable = a.getBoolean( R.styleable.Keyboard_Key_isRepeatable, false); modifier = a.getBoolean( R.styleable.Keyboard_Key_isModifier, false); sticky = a.getBoolean( R.styleable.Keyboard_Key_isSticky, false); isCursor = a.getBoolean( R.styleable.Keyboard_Key_isCursor, false); icon = a.getDrawable( R.styleable.Keyboard_Key_keyIcon); if (icon != null) { icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); } label = a.getText(R.styleable.Keyboard_Key_keyLabel); shiftLabel = a.getText(R.styleable.Keyboard_Key_shiftLabel); if (shiftLabel != null && shiftLabel.length() == 0) shiftLabel = null; capsLabel = a.getText(R.styleable.Keyboard_Key_capsLabel); if (capsLabel != null && capsLabel.length() == 0) capsLabel = null; text = a.getText(R.styleable.Keyboard_Key_keyOutputText); if (codes == null && !TextUtils.isEmpty(label)) { codes = getFromString(label); if (codes != null && codes.length == 1) { final Locale locale = LatinIME.sKeyboardSettings.inputLocale; String upperLabel = label.toString().toUpperCase(locale); if (shiftLabel == null) { // No shiftLabel supplied, auto-set to uppercase if possible. if (!upperLabel.equals(label.toString()) && upperLabel.length() == 1) { shiftLabel = upperLabel; isSimpleUppercase = true; } } else { // Both label and shiftLabel supplied. Check if // the shiftLabel is the uppercased normal label. // If not, treat it as a distinct uppercase variant. if (capsLabel != null) { isDistinctUppercase = true; } else if (upperLabel.equals(shiftLabel.toString())) { isSimpleUppercase = true; } else if (upperLabel.length() == 1) { capsLabel = upperLabel; isDistinctUppercase = true; } } } if ((LatinIME.sKeyboardSettings.popupKeyboardFlags & POPUP_DISABLE) != 0) { popupCharacters = null; popupResId = 0; } if ((LatinIME.sKeyboardSettings.popupKeyboardFlags & POPUP_AUTOREPEAT) != 0) { // Assume POPUP_DISABLED is set too, otherwise things may get weird. repeatable = true; } } //Log.i(TAG, "added key definition: " + this); a.recycle(); } public boolean isDistinctCaps() { return isDistinctUppercase && keyboard.isShiftCaps(); } public boolean isShifted() { boolean shifted = keyboard.isShifted(isSimpleUppercase); //Log.i(TAG, "FIXME isShifted=" + shifted + " for " + this); return shifted; } public int getPrimaryCode(boolean isShiftCaps, boolean isShifted) { if (isDistinctUppercase && isShiftCaps) { return capsLabel.charAt(0); } //Log.i(TAG, "getPrimaryCode(), shifted=" + shifted); if (isShifted && shiftLabel != null) { if (shiftLabel.charAt(0) == DEAD_KEY_PLACEHOLDER && shiftLabel.length() >= 2) { return shiftLabel.charAt(1); } else { return shiftLabel.charAt(0); } } else { return codes[0]; } } public int getPrimaryCode() { return getPrimaryCode(keyboard.isShiftCaps(), keyboard.isShifted(isSimpleUppercase)); } public boolean isDeadKey() { if (codes == null || codes.length < 1) return false; return Character.getType(codes[0]) == Character.NON_SPACING_MARK; } public int[] getFromString(CharSequence str) { if (str.length() > 1) { if (str.charAt(0) == DEAD_KEY_PLACEHOLDER && str.length() >= 2) { return new int[] { str.charAt(1) }; // FIXME: >1 length? } else { text = str; // TODO: add space? return new int[] { 0 }; } } else { char c = str.charAt(0); return new int[] { c }; } } public String getCaseLabel() { if (isDistinctUppercase && keyboard.isShiftCaps()) { return capsLabel.toString(); } boolean isShifted = keyboard.isShifted(isSimpleUppercase); if (isShifted && shiftLabel != null) { return shiftLabel.toString(); } else { return label != null ? label.toString() : null; } } private String getPopupKeyboardContent(boolean isShiftCaps, boolean isShifted, boolean addExtra) { int mainChar = getPrimaryCode(false, false); int shiftChar = getPrimaryCode(false, true); int capsChar = getPrimaryCode(true, true); // Remove duplicates if (shiftChar == mainChar) shiftChar = 0; if (capsChar == shiftChar || capsChar == mainChar) capsChar = 0; int popupLen = (popupCharacters == null) ? 0 : popupCharacters.length(); StringBuilder popup = new StringBuilder(popupLen); for (int i = 0; i < popupLen; ++i) { char c = popupCharacters.charAt(i); if (isShifted || isShiftCaps) { String upper = Character.toString(c).toUpperCase(LatinIME.sKeyboardSettings.inputLocale); if (upper.length() == 1) c = upper.charAt(0); } if (c == mainChar || c == shiftChar || c == capsChar) continue; popup.append(c); } if (addExtra) { StringBuilder extra = new StringBuilder(3 + popup.length()); int flags = LatinIME.sKeyboardSettings.popupKeyboardFlags; if ((flags & POPUP_ADD_SELF) != 0) { // if shifted, add unshifted key to extra, and vice versa if (isDistinctUppercase && isShiftCaps) { if (capsChar > 0) { extra.append((char) capsChar); capsChar = 0; } } else if (isShifted) { if (shiftChar > 0) { extra.append((char) shiftChar); shiftChar = 0; } } else { if (mainChar > 0) { extra.append((char) mainChar); mainChar = 0; } } } if ((flags & POPUP_ADD_CASE) != 0) { // if shifted, add unshifted key to popup, and vice versa if (isDistinctUppercase && isShiftCaps) { if (mainChar > 0) { extra.append((char) mainChar); mainChar = 0; } if (shiftChar > 0) { extra.append((char) shiftChar); shiftChar = 0; } } else if (isShifted) { if (mainChar > 0) { extra.append((char) mainChar); mainChar = 0; } if (capsChar > 0) { extra.append((char) capsChar); capsChar = 0; } } else { if (shiftChar > 0) { extra.append((char) shiftChar); shiftChar = 0; } if (capsChar > 0) { extra.append((char) capsChar); capsChar = 0; } } } if (!isSimpleUppercase && (flags & POPUP_ADD_SHIFT) != 0) { // if shifted, add unshifted key to popup, and vice versa if (isShifted) { if (mainChar > 0) { extra.append((char) mainChar); mainChar = 0; } } else { if (shiftChar > 0) { extra.append((char) shiftChar); shiftChar = 0; } } } extra.append(popup); return extra.toString(); } return popup.toString(); } public Keyboard getPopupKeyboard(Context context, int padding) { if (popupCharacters == null) { if (popupResId != 0) { return new Keyboard(context, keyboard.mDefaultHeight, popupResId); } else { if (modifier) return null; // Space, Return etc. } } if ((LatinIME.sKeyboardSettings.popupKeyboardFlags & POPUP_DISABLE) != 0) return null; String popup = getPopupKeyboardContent(keyboard.isShiftCaps(), keyboard.isShifted(isSimpleUppercase), true); //Log.i(TAG, "getPopupKeyboard: popup='" + popup + "' for " + this); if (popup.length() > 0) { int resId = popupResId; if (resId == 0) resId = R.xml.kbd_popup_template; return new Keyboard(context, keyboard.mDefaultHeight, resId, popup, popupReversed, -1, padding); } else { return null; } } public String getHintLabel(boolean wantAscii, boolean wantAll) { if (hint == null) { hint = ""; if (shiftLabel != null && !isSimpleUppercase) { char c = shiftLabel.charAt(0); if (wantAll || wantAscii && is7BitAscii(c)) { hint = Character.toString(c); } } } return hint; } public String getAltHintLabel(boolean wantAscii, boolean wantAll) { if (altHint == null) { altHint = ""; String popup = getPopupKeyboardContent(false, false, false); if (popup.length() > 0) { char c = popup.charAt(0); if (wantAll || wantAscii && is7BitAscii(c)) { altHint = Character.toString(c); } } } return altHint; } private static boolean is7BitAscii(char c) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) return false; return c >= 32 && c < 127; } /** * Informs the key that it has been pressed, in case it needs to change its appearance or * state. * @see #onReleased(boolean) */ public void onPressed() { pressed = !pressed; } /** * Changes the pressed state of the key. Sticky key indicators are handled explicitly elsewhere. * @param inside whether the finger was released inside the key * @see #onPressed() */ public void onReleased(boolean inside) { pressed = !pressed; } int[] parseCSV(String value) { int count = 0; int lastIndex = 0; if (value.length() > 0) { count++; while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { count++; } } int[] values = new int[count]; count = 0; StringTokenizer st = new StringTokenizer(value, ","); while (st.hasMoreTokens()) { try { values[count++] = Integer.parseInt(st.nextToken()); } catch (NumberFormatException nfe) { Log.e(TAG, "Error parsing keycodes " + value); } } return values; } /** * Detects if a point falls inside this key. * @param x the x-coordinate of the point * @param y the y-coordinate of the point * @return whether or not the point falls inside the key. If the key is attached to an edge, * it will assume that all points between the key and the edge are considered to be inside * the key. */ public boolean isInside(int x, int y) { boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; boolean topEdge = (edgeFlags & EDGE_TOP) > 0; boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; if ((x >= this.x || (leftEdge && x <= this.x + this.width)) && (x < this.x + this.width || (rightEdge && x >= this.x)) && (y >= this.y || (topEdge && y <= this.y + this.height)) && (y < this.y + this.height || (bottomEdge && y >= this.y))) { return true; } else { return false; } } /** * Returns the square of the distance between the center of the key and the given point. * @param x the x-coordinate of the point * @param y the y-coordinate of the point * @return the square of the distance of the point from the center of the key */ public int squaredDistanceFrom(int x, int y) { int xDist = this.x + width / 2 - x; int yDist = this.y + height / 2 - y; return xDist * xDist + yDist * yDist; } /** * Returns the drawable state for the key, based on the current state and type of the key. * @return the drawable state of the key. * @see android.graphics.drawable.StateListDrawable#setState(int[]) */ public int[] getCurrentDrawableState() { int[] states = KEY_STATE_NORMAL; if (locked) { if (pressed) { states = KEY_STATE_PRESSED_LOCK; } else { states = KEY_STATE_NORMAL_LOCK; } } else if (on) { if (pressed) { states = KEY_STATE_PRESSED_ON; } else { states = KEY_STATE_NORMAL_ON; } } else { if (sticky) { if (pressed) { states = KEY_STATE_PRESSED_OFF; } else { states = KEY_STATE_NORMAL_OFF; } } else { if (pressed) { states = KEY_STATE_PRESSED; } } } return states; } public String toString() { int code = (codes != null && codes.length > 0) ? codes[0] : 0; String edges = ( ((edgeFlags & Keyboard.EDGE_LEFT) != 0 ? "L" : "-") + ((edgeFlags & Keyboard.EDGE_RIGHT) != 0 ? "R" : "-") + ((edgeFlags & Keyboard.EDGE_TOP) != 0 ? "T" : "-") + ((edgeFlags & Keyboard.EDGE_BOTTOM) != 0 ? "B" : "-")); return "KeyDebugFIXME(label=" + label + (shiftLabel != null ? " shift=" + shiftLabel : "") + (capsLabel != null ? " caps=" + capsLabel : "") + (text != null ? " text=" + text : "" ) + " code=" + code + (code <= 0 || Character.isWhitespace(code) ? "" : ":'" + (char)code + "'" ) + " x=" + x + ".." + (x+width) + " y=" + y + ".." + (y+height) + " edgeFlags=" + edges + (popupCharacters != null ? " pop=" + popupCharacters : "" ) + " res=" + popupResId + ")"; } } /** * Creates a keyboard from the given xml key layout file. * @param context the application or service context * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. */ public Keyboard(Context context, int defaultHeight, int xmlLayoutResId) { this(context, defaultHeight, xmlLayoutResId, 0); } public Keyboard(Context context, int defaultHeight, int xmlLayoutResId, int modeId) { this(context, defaultHeight, xmlLayoutResId, modeId, 0); } /** * Creates a keyboard from the given xml key layout file. Weeds out rows * that have a keyboard mode defined but don't match the specified mode. * @param context the application or service context * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. * @param modeId keyboard mode identifier * @param rowHeightPercent height of each row as percentage of screen height */ public Keyboard(Context context, int defaultHeight, int xmlLayoutResId, int modeId, float kbHeightPercent) { DisplayMetrics dm = context.getResources().getDisplayMetrics(); mDisplayWidth = dm.widthPixels; mDisplayHeight = dm.heightPixels; Log.v(TAG, "keyboard's display metrics:" + dm + ", mDisplayWidth=" + mDisplayWidth); mDefaultHorizontalGap = 0; mDefaultWidth = mDisplayWidth / 10; mDefaultVerticalGap = 0; mDefaultHeight = defaultHeight; // may be zero, to be adjusted below mKeyboardHeight = Math.round(mDisplayHeight * kbHeightPercent / 100); //Log.i("PCKeyboard", "mDefaultHeight=" + mDefaultHeight + "(arg=" + defaultHeight + ")" + " kbHeight=" + mKeyboardHeight + " displayHeight="+mDisplayHeight+")"); mKeys = new ArrayList<Key>(); mModifierKeys = new ArrayList<Key>(); mKeyboardMode = modeId; mUseExtension = LatinIME.sKeyboardSettings.useExtension; loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); setEdgeFlags(); fixAltChars(LatinIME.sKeyboardSettings.inputLocale); } /** * <p>Creates a blank keyboard from the given resource file and populates it with the specified * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. * </p> * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as * possible in each row.</p> * @param context the application or service context * @param layoutTemplateResId the layout template file, containing no keys. * @param characters the list of characters to display on the keyboard. One key will be created * for each character. * @param columns the number of columns of keys to display. If this number is greater than the * number of keys that can fit in a row, it will be ignored. If this number is -1, the * keyboard will fit as many keys as possible in each row. */ private Keyboard(Context context, int defaultHeight, int layoutTemplateResId, CharSequence characters, boolean reversed, int columns, int horizontalPadding) { this(context, defaultHeight, layoutTemplateResId); int x = 0; int y = 0; int column = 0; mTotalWidth = 0; Row row = new Row(this); row.defaultHeight = mDefaultHeight; row.defaultWidth = mDefaultWidth; row.defaultHorizontalGap = mDefaultHorizontalGap; row.verticalGap = mDefaultVerticalGap; final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; mLayoutRows = 1; int start = reversed ? characters.length()-1 : 0; int end = reversed ? -1 : characters.length(); int step = reversed ? -1 : 1; for (int i = start; i != end; i+=step) { char c = characters.charAt(i); if (column >= maxColumns || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { x = 0; y += mDefaultVerticalGap + mDefaultHeight; column = 0; ++mLayoutRows; } final Key key = new Key(row); key.x = x; key.realX = x; key.y = y; key.label = String.valueOf(c); key.codes = key.getFromString(key.label); column++; x += key.width + key.gap; mKeys.add(key); if (x > mTotalWidth) { mTotalWidth = x; } } mTotalHeight = y + mDefaultHeight; mLayoutColumns = columns == -1 ? column : maxColumns; setEdgeFlags(); } private void setEdgeFlags() { if (mRowCount == 0) mRowCount = 1; // Assume one row if not set int row = 0; Key prevKey = null; int rowFlags = 0; for (Key key : mKeys) { int keyFlags = 0; if (prevKey == null || key.x <= prevKey.x) { // Start new row. if (prevKey != null) { // Add "right edge" to rightmost key of previous row. // Need to do the last key separately below. prevKey.edgeFlags |= Keyboard.EDGE_RIGHT; } // Set the row flags for the current row. rowFlags = 0; if (row == 0) rowFlags |= Keyboard.EDGE_TOP; if (row == mRowCount - 1) rowFlags |= Keyboard.EDGE_BOTTOM; ++row; // Mark current key as "left edge" keyFlags |= Keyboard.EDGE_LEFT; } key.edgeFlags = rowFlags | keyFlags; prevKey = key; } // Fix up the last key if (prevKey != null) prevKey.edgeFlags |= Keyboard.EDGE_RIGHT; // Log.i(TAG, "setEdgeFlags() done:"); // for (Key key : mKeys) { // Log.i(TAG, "key=" + key); // } } private void fixAltChars(Locale locale) { if (locale == null) locale = Locale.getDefault(); Set<Character> mainKeys = new HashSet<Character>(); for (Key key : mKeys) { // Remember characters on the main keyboard so that they can be removed from popups. // This makes it easy to share popup char maps between the normal and shifted // keyboards. if (key.label != null && !key.modifier && key.label.length() == 1) { char c = key.label.charAt(0); mainKeys.add(c); } } for (Key key : mKeys) { if (key.popupCharacters == null) continue; int popupLen = key.popupCharacters.length(); if (popupLen == 0) { continue; } if (key.x >= mTotalWidth / 2) { key.popupReversed = true; } // Uppercase the alt chars if the main key is uppercase boolean needUpcase = key.label != null && key.label.length() == 1 && Character.isUpperCase(key.label.charAt(0)); if (needUpcase) { key.popupCharacters = key.popupCharacters.toString().toUpperCase(); popupLen = key.popupCharacters.length(); } StringBuilder newPopup = new StringBuilder(popupLen); for (int i = 0; i < popupLen; ++i) { char c = key.popupCharacters.charAt(i); if (Character.isDigit(c) && mainKeys.contains(c)) continue; // already present elsewhere // Skip extra digit alt keys on 5-row keyboards if ((key.edgeFlags & EDGE_TOP) == 0 && Character.isDigit(c)) continue; newPopup.append(c); } //Log.i("PCKeyboard", "popup for " + key.label + " '" + key.popupCharacters + "' => '"+ newPopup + "' length " + newPopup.length()); key.popupCharacters = newPopup.toString(); } } public List<Key> getKeys() { return mKeys; } public List<Key> getModifierKeys() { return mModifierKeys; } protected int getHorizontalGap() { return Math.round(mDefaultHorizontalGap); } protected void setHorizontalGap(int gap) { mDefaultHorizontalGap = gap; } protected int getVerticalGap() { return mDefaultVerticalGap; } protected void setVerticalGap(int gap) { mDefaultVerticalGap = gap; } protected int getKeyHeight() { return mDefaultHeight; } protected void setKeyHeight(int height) { mDefaultHeight = height; } protected int getKeyWidth() { return Math.round(mDefaultWidth); } protected void setKeyWidth(int width) { mDefaultWidth = width; } /** * Returns the total height of the keyboard * @return the total height of the keyboard */ public int getHeight() { return mTotalHeight; } public int getScreenHeight() { return mDisplayHeight; } public int getMinWidth() { return mTotalWidth; } public boolean setShiftState(int shiftState, boolean updateKey) { //Log.i(TAG, "setShiftState " + mShiftState + " -> " + shiftState); if (updateKey && mShiftKey != null) { mShiftKey.on = (shiftState != SHIFT_OFF); } if (mShiftState != shiftState) { mShiftState = shiftState; return true; } return false; } public boolean setShiftState(int shiftState) { return setShiftState(shiftState, true); } public Key setCtrlIndicator(boolean active) { //Log.i(TAG, "setCtrlIndicator " + active + " ctrlKey=" + mCtrlKey); if (mCtrlKey != null) mCtrlKey.on = active; return mCtrlKey; } public Key setAltIndicator(boolean active) { if (mAltKey != null) mAltKey.on = active; return mAltKey; } public Key setMetaIndicator(boolean active) { if (mMetaKey != null) mMetaKey.on = active; return mMetaKey; } public boolean isShiftCaps() { return mShiftState == SHIFT_CAPS || mShiftState == SHIFT_CAPS_LOCKED; } public boolean isShifted(boolean applyCaps) { if (applyCaps) { return mShiftState != SHIFT_OFF; } else { return mShiftState == SHIFT_ON || mShiftState == SHIFT_LOCKED; } } public int getShiftState() { return mShiftState; } public int getShiftKeyIndex() { return mShiftKeyIndex; } private void computeNearestNeighbors() { // Round-up so we don't have any pixels outside the grid mCellWidth = (getMinWidth() + mLayoutColumns - 1) / mLayoutColumns; mCellHeight = (getHeight() + mLayoutRows - 1) / mLayoutRows; mGridNeighbors = new int[mLayoutColumns * mLayoutRows][]; int[] indices = new int[mKeys.size()]; final int gridWidth = mLayoutColumns * mCellWidth; final int gridHeight = mLayoutRows * mCellHeight; for (int x = 0; x < gridWidth; x += mCellWidth) { for (int y = 0; y < gridHeight; y += mCellHeight) { int count = 0; for (int i = 0; i < mKeys.size(); i++) { final Key key = mKeys.get(i); boolean isSpace = key.codes != null && key.codes.length > 0 && key.codes[0] == LatinIME.ASCII_SPACE; if (key.squaredDistanceFrom(x, y) < mProximityThreshold || key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold || key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) < mProximityThreshold || key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold || isSpace && !( x + mCellWidth - 1 < key.x || x > key.x + key.width || y + mCellHeight - 1 < key.y || y > key.y + key.height)) { //if (isSpace) Log.i(TAG, "space at grid" + x + "," + y); indices[count++] = i; } } int [] cell = new int[count]; System.arraycopy(indices, 0, cell, 0, count); mGridNeighbors[(y / mCellHeight) * mLayoutColumns + (x / mCellWidth)] = cell; } } } /** * Returns the indices of the keys that are closest to the given point. * @param x the x-coordinate of the point * @param y the y-coordinate of the point * @return the array of integer indices for the nearest keys to the given point. If the given * point is out of range, then an array of size zero is returned. */ public int[] getNearestKeys(int x, int y) { if (mGridNeighbors == null) computeNearestNeighbors(); if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) { int index = (y / mCellHeight) * mLayoutColumns + (x / mCellWidth); if (index < mLayoutRows * mLayoutColumns) { return mGridNeighbors[index]; } } return new int[0]; } protected Row createRowFromXml(Resources res, XmlResourceParser parser) { return new Row(res, this, parser); } protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { return new Key(res, parent, x, y, parser); } private void loadKeyboard(Context context, XmlResourceParser parser) { boolean inKey = false; boolean inRow = false; float x = 0; int y = 0; Key key = null; Row currentRow = null; Resources res = context.getResources(); boolean skipRow = false; mRowCount = 0; try { int event; Key prevKey = null; while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { if (event == XmlResourceParser.START_TAG) { String tag = parser.getName(); if (TAG_ROW.equals(tag)) { inRow = true; x = 0; currentRow = createRowFromXml(res, parser); skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode; if (currentRow.extension) { if (mUseExtension) { ++mExtensionRowCount; } else { skipRow = true; } } if (skipRow) { skipToEndOfRow(parser); inRow = false; } } else if (TAG_KEY.equals(tag)) { inKey = true; key = createKeyFromXml(res, currentRow, Math.round(x), y, parser); key.realX = x; if (key.codes == null) { // skip this key, adding its width to the previous one if (prevKey != null) { prevKey.width += key.width; } } else { mKeys.add(key); prevKey = key; if (key.codes[0] == KEYCODE_SHIFT) { if (mShiftKeyIndex == -1) { mShiftKey = key; mShiftKeyIndex = mKeys.size()-1; } mModifierKeys.add(key); } else if (key.codes[0] == KEYCODE_ALT_SYM) { mModifierKeys.add(key); } else if (key.codes[0] == LatinKeyboardView.KEYCODE_CTRL_LEFT) { mCtrlKey = key; } else if (key.codes[0] == LatinKeyboardView.KEYCODE_ALT_LEFT) { mAltKey = key; } else if (key.codes[0] == LatinKeyboardView.KEYCODE_META_LEFT) { mMetaKey = key; } } } else if (TAG_KEYBOARD.equals(tag)) { parseKeyboardAttributes(res, parser); } } else if (event == XmlResourceParser.END_TAG) { if (inKey) { inKey = false; x += key.realGap + key.realWidth; if (x > mTotalWidth) { mTotalWidth = Math.round(x); } } else if (inRow) { inRow = false; y += currentRow.verticalGap; y += currentRow.defaultHeight; mRowCount++; } else { // TODO: error or extend? } } } } catch (Exception e) { Log.e(TAG, "Parse error:" + e); e.printStackTrace(); } mTotalHeight = y - mDefaultVerticalGap; } public void setKeyboardWidth(int newWidth) { Log.i(TAG, "setKeyboardWidth newWidth=" + newWidth + ", mTotalWidth=" + mTotalWidth); if (newWidth <= 0) return; // view not initialized? if (mTotalWidth <= newWidth) return; // it already fits float scale = (float) newWidth / mDisplayWidth; Log.i("PCKeyboard", "Rescaling keyboard: " + mTotalWidth + " => " + newWidth); for (Key key : mKeys) { key.x = Math.round(key.realX * scale); } mTotalWidth = newWidth; } private void skipToEndOfRow(XmlResourceParser parser) throws XmlPullParserException, IOException { int event; while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { if (event == XmlResourceParser.END_TAG && parser.getName().equals(TAG_ROW)) { break; } } } private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) { TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard); mDefaultWidth = getDimensionOrFraction(a, R.styleable.Keyboard_keyWidth, mDisplayWidth, mDisplayWidth / 10); mDefaultHeight = Math.round(getDimensionOrFraction(a, R.styleable.Keyboard_keyHeight, mDisplayHeight, mDefaultHeight)); mDefaultHorizontalGap = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalGap, mDisplayWidth, 0); mDefaultVerticalGap = Math.round(getDimensionOrFraction(a, R.styleable.Keyboard_verticalGap, mDisplayHeight, 0)); mHorizontalPad = getDimensionOrFraction(a, R.styleable.Keyboard_horizontalPad, mDisplayWidth, res.getDimension(R.dimen.key_horizontal_pad)); mVerticalPad = getDimensionOrFraction(a, R.styleable.Keyboard_verticalPad, mDisplayHeight, res.getDimension(R.dimen.key_vertical_pad)); mLayoutRows = a.getInteger(R.styleable.Keyboard_layoutRows, DEFAULT_LAYOUT_ROWS); mLayoutColumns = a.getInteger(R.styleable.Keyboard_layoutColumns, DEFAULT_LAYOUT_COLUMNS); if (mDefaultHeight == 0 && mKeyboardHeight > 0 && mLayoutRows > 0) { mDefaultHeight = mKeyboardHeight / mLayoutRows; //Log.i(TAG, "got mLayoutRows=" + mLayoutRows + ", mDefaultHeight=" + mDefaultHeight); } mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE); mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison a.recycle(); } static float getDimensionOrFraction(TypedArray a, int index, int base, float defValue) { TypedValue value = a.peekValue(index); if (value == null) return defValue; if (value.type == TypedValue.TYPE_DIMENSION) { return a.getDimensionPixelOffset(index, Math.round(defValue)); } else if (value.type == TypedValue.TYPE_FRACTION) { // Round it to avoid values like 47.9999 from getting truncated //return Math.round(a.getFraction(index, base, base, defValue)); return a.getFraction(index, base, base, defValue); } return defValue; } @Override public String toString() { return "Keyboard(" + mLayoutColumns + "x" + mLayoutRows + " keys=" + mKeys.size() + " rowCount=" + mRowCount + " mode=" + mKeyboardMode + " size=" + mTotalWidth + "x" + mTotalHeight + ")"; } }