/*
* Copyright (C) 2008-2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.s16.inputmethod.skeyboard;
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.Paint;
import android.graphics.Rect;
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.List;
import java.util.StringTokenizer;
import com.s16.inputmethod.skeyboard.R;
/**
* 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";
static final boolean DEBUG = false;
static final boolean TRACE = false;
// 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 ROW_NORMAL = 0x00;
public static final int ROW_EXTENDED = 0x01;
/** Keyboard label **/
protected CharSequence mLabel;
/** Horizontal gap default for all rows */
protected float mDefaultHorizontalGap;
/** Default key width */
protected float mDefaultWidth;
/** Default key height */
protected int mDefaultHeight;
/** Default gap between rows */
protected int mDefaultVerticalGap;
/** Default preview popup disable */
protected boolean mDefaultDisablePreview; // SMM
/** Default Shift Locked mode **/
protected boolean mDefaultShiftCapsMode; // SMM
/** Is the keyboard in the shifted state */
protected boolean mShifted;
/** Key instance for the shift key, if present */
protected Key[] mShiftKeys = { null, null };
/** Key index for the shift key, if present */
protected int[] mShiftKeyIndices = {-1, -1};
/** Current key width, while loading the keyboard */
protected int mKeyWidth;
/** Current key height, while loading the keyboard */
protected int mKeyHeight;
/** Total height of the keyboard, including the padding and keys */
protected int mTotalHeight;
/**
* Total width of the keyboard, including left side gaps and keys, but not any gaps on the
* right side.
*/
protected int mTotalWidth;
/** List of keys in this keyboard */
protected List<Key> mKeys;
/** List of modifier keys such as Shift & Alt, if any */
protected List<Key> mModifierKeys;
/** Width of the screen available to fit the keyboard */
protected int mDisplayWidth;
/** Height of the screen */
protected int mDisplayHeight;
/** Keyboard mode, or zero, if none. */
protected int mKeyboardMode;
/** Key text weight, def 2. */
protected int mKeyTextWeight;
/** Maximum height to fit the keyboard */
protected int mMaxHeight;
/** Minimum height to fit the keyboard */
protected int mMinHeight;
//protected KeyboardIconsSet mIconsSet;
// Variables for pre-computing nearest keys.
private static final int GRID_WIDTH = 10;
private static final int GRID_HEIGHT = 5;
private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
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;
private ArrayList<Row> rows = new ArrayList<Row>();
/**
* 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_rowEdgeFlags
* @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;
/** Row type for this row. */
public int rowType;
ArrayList<Key> mKeys = new ArrayList<Key>();
/**
* Edge flags for this row of keys. Possible values that can be assigned are
* {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
*/
public int rowEdgeFlags;
/** The keyboard mode for this row */
public int mode;
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 = getDimensionOrFractionInt(a,
R.styleable.Keyboard_keyHeight,
parent.mDisplayHeight, parent.mDefaultHeight);
defaultHorizontalGap = getDimensionOrFraction(a,
R.styleable.Keyboard_horizontalGap,
parent.mDisplayWidth, parent.mDefaultHorizontalGap);
verticalGap = getDimensionOrFractionInt(a,
R.styleable.Keyboard_verticalGap,
parent.mDisplayHeight, parent.mDefaultVerticalGap);
a.recycle();
a = res.obtainAttributes(Xml.asAttributeSet(parser),
R.styleable.Keyboard_Row);
rowEdgeFlags = a.getInt(R.styleable.Keyboard_Row_rowEdgeFlags, 0);
mode = a.getResourceId(R.styleable.Keyboard_Row_keyboardMode, 0);
rowType = a.getInt(R.styleable.Keyboard_Row_rowType, 0);
if (rowType == ROW_EXTENDED) {
defaultHeight = (int)(((float)defaultHeight / 100.0f) * 80.0f);
}
}
}
/**
* 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_shiftLabel
* @attr ref android.R.styleable#Keyboard_Key_capsLabel
* @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_isCursor
* @attr ref android.R.styleable#Keyboard_Key_popupKeyboard
* @attr ref android.R.styleable#Keyboard_Key_popupCharacters
* @attr ref android.R.styleable#Keyboard_Key_keyOutputText
* @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags
*/
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;
public int[] shiftCodes;
/** Label to display */
public CharSequence label;
public CharSequence shiftLabel;
public CharSequence hintLabel;
public CharSequence trailLabel;
public int iconId;
/** 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 */
public int height;
/** The horizontal gap before this key */
public int gap;
/** Whether this key is sticky, i.e., a toggle key */
public boolean sticky;
/** Whether this key is spacer */
public boolean spacer;
/** X coordinate of the key in the keyboard layout */
public int x;
/** 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? */
public boolean on;
/** If this is a action key */
public boolean actionKey;
/** Text to output when pressed. This can be multiple characters, like ".com" */
public CharSequence text;
/** Popup characters */
public CharSequence popupCharacters;
// SMM {
/** Disable text size. */
public boolean fullTextSize;
/** Used Iconic font. */
public boolean iconic;
/** Iconic Key. */
public boolean iconKey;
/** Hide popup preview. */
public boolean hidePopupPreview;
public float iconSizeAdjust;
private float realWidth;
private float realGap;
// } SMM
/**
* 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;
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_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_ACTIVE = {
android.R.attr.state_active
};
private final static int[] KEY_STATE_ACTIVE_PRESSED = {
android.R.attr.state_pressed,
android.R.attr.state_active
};
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; // SMM
gap = Math.round(parent.defaultHorizontalGap);
edgeFlags = parent.rowEdgeFlags;
iconSizeAdjust = 0.6f;
}
/** 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);
height = Math.round(realHeight);
realGap = getDimensionOrFraction(a,
R.styleable.Keyboard_horizontalGap,
keyboard.mDisplayWidth, parent.defaultHorizontalGap);
width = Math.round(realWidth);
gap = Math.round(realGap);
/*width = getDimensionOrFraction(a,
R.styleable.Keyboard_keyWidth,
keyboard.mDisplayWidth, parent.defaultWidth);
height = getDimensionOrFraction(a,
R.styleable.Keyboard_keyHeight,
keyboard.mDisplayHeight, parent.defaultHeight);
gap = getDimensionOrFraction(a,
R.styleable.Keyboard_horizontalGap,
keyboard.mDisplayWidth, parent.defaultHorizontalGap);*/
a.recycle();
a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
this.x += gap;
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());
}
TypedValue shiftCodesValue = new TypedValue();
a.getValue(R.styleable.Keyboard_Key_shiftCodes, shiftCodesValue);
if (shiftCodesValue.type == TypedValue.TYPE_INT_DEC
|| shiftCodesValue.type == TypedValue.TYPE_INT_HEX) {
shiftCodes = new int[] { shiftCodesValue.data };
} else if (shiftCodesValue.type == TypedValue.TYPE_STRING) {
shiftCodes = parseCSV(shiftCodesValue.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);
actionKey = a.getBoolean(R.styleable.Keyboard_Key_isAction, false);
spacer = a.getBoolean(R.styleable.Keyboard_Key_isSpacer, false);
edgeFlags = a.getInt(R.styleable.Keyboard_Key_keyEdgeFlags, 0);
edgeFlags |= parent.rowEdgeFlags;
iconId = a.getInt(R.styleable.Keyboard_Key_keyIconId, 0); // SMM
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);
trailLabel = a.getText(R.styleable.Keyboard_Key_trailLabel);
text = a.getText(R.styleable.Keyboard_Key_keyOutputText);
fullTextSize = a.getBoolean(R.styleable.Keyboard_Key_fullTextSize, false);
iconic = a.getBoolean(R.styleable.Keyboard_Key_isIconic, false);
hidePopupPreview = a.getBoolean(R.styleable.Keyboard_Key_hidePopupPreview, false) || spacer;
a.recycle();
if (codes == null) {
if (!TextUtils.isEmpty(label)) {
codes = new int[] { label.charAt(0) };
} else {
codes = new int[] { 0 };
}
}
if(shiftCodes == null) {
if (!TextUtils.isEmpty(shiftLabel)) {
shiftCodes = new int[] { shiftLabel.charAt(0) };
} else {
shiftCodes = codes;
}
}
if(popupCharacters != null && !TextUtils.isEmpty(popupCharacters)) {
if(KeyboardBaseView.isAsciiDigit(popupCharacters.charAt(0))) {
hintLabel = String.valueOf(popupCharacters.charAt(0));
} else if(KeyboardBaseView.isAsciiDigit(popupCharacters.charAt(popupCharacters.length() - 1))) {
hintLabel = String.valueOf(popupCharacters.charAt(popupCharacters.length() - 1));
}
}
}
/**
* 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. If it is a sticky key, it will also change the
* toggled state of the key if the finger was release inside.
* @param inside whether the finger was released inside the key
* @see #onPressed()
*/
public void onReleased(boolean inside) {
pressed = !pressed;
if (sticky) {
on = !on;
}
}
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 (on) {
if (pressed) {
states = KEY_STATE_PRESSED_ON;
} else {
states = KEY_STATE_NORMAL_ON;
}
} else if (actionKey) {
if (pressed) {
states = KEY_STATE_ACTIVE_PRESSED;
} else {
states = KEY_STATE_ACTIVE;
}
} 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 boolean isShifted() {
return keyboard.isShifted();
}
public boolean isSpaceKey() {
if (codes == null) {
return false;
}
return codes[0] == KeyCodes.KEYCODE_SPACE;
}
public CharSequence getLabel(boolean adjustCase) {
CharSequence retLabel = label != null ? label : null;
if (adjustCase && keyboard.isShifted() && label != null && label.length() < 3) {
if(shiftLabel != null) {
retLabel = shiftLabel;
} else if(Character.isLowerCase(label.charAt(0))) {
retLabel = label.toString().toUpperCase();
}
}
if(retLabel != null && retLabel.length() == 1) {
retLabel = DeadAccentSequence.getSpacing(retLabel.charAt(0));
}
return retLabel;
}
public int getCode() {
if (codes == null) {
return 0;
}
int code = codes[0];
if(keyboard.isShifted() && shiftCodes != null && shiftCodes.length == 1) {
code = shiftCodes[0];
}
return code;
}
public int[] getCodes() {
if(keyboard.isShifted() && shiftCodes != null && shiftCodes.length == 1) {
return shiftCodes;
}
return codes;
}
}
/**
* 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 xmlLayoutResId) {
this(context, xmlLayoutResId, 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 width sets width of keyboard
* @param height sets height of keyboard
*/
public Keyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) {
mDisplayWidth = width;
mDisplayHeight = height;
mDefaultHorizontalGap = 0;
mDefaultWidth = mDisplayWidth / 10;
mDefaultVerticalGap = 0;
mDefaultHeight = Math.round(mDefaultWidth);
mKeys = new ArrayList<Key>();
mModifierKeys = new ArrayList<Key>();
mKeyboardMode = modeId;
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
}
/**
* 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
*/
public Keyboard(Context context, int xmlLayoutResId, int modeId) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
mDisplayWidth = dm.widthPixels;
mDisplayHeight = dm.heightPixels;
if(DEBUG) {
Log.v(TAG, "keyboard's display metrics:" + dm);
}
mDefaultHorizontalGap = 0;
mDefaultWidth = mDisplayWidth / 8;
mDefaultVerticalGap = 0;
mDefaultHeight = Math.round(mDefaultWidth);
mKeys = new ArrayList<Key>();
mModifierKeys = new ArrayList<Key>();
mKeyboardMode = modeId;
loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
}
/**
* <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.
*/
public Keyboard(Context context, int layoutTemplateResId,
CharSequence characters, int columns, int horizontalPadding) {
this(context, 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;
row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
for (int i = 0; i < characters.length(); i++) {
char c = characters.charAt(i);
if (column >= maxColumns
|| x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
x = 0;
y += mDefaultVerticalGap + mDefaultHeight;
column = 0;
}
final Key key = new Key(row);
key.x = x;
key.y = y;
key.label = String.valueOf(c);
key.codes = new int[] { c };
column++;
x += key.width + key.gap;
mKeys.add(key);
row.mKeys.add(key);
if (x > mTotalWidth) {
mTotalWidth = x;
}
}
mTotalHeight = y + mDefaultHeight;
rows.add(row);
}
final void resize(int newWidth, int newHeight) {
if (newWidth > 0 && newWidth != mTotalWidth) {
int numRows = rows.size();
for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
Row row = rows.get(rowIndex);
int numKeys = row.mKeys.size();
int totalGap = 0;
int totalWidth = 0;
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
Key key = row.mKeys.get(keyIndex);
if (keyIndex > 0) {
totalGap += key.gap;
}
totalWidth += key.width;
}
if (totalGap + totalWidth > newWidth) {
int x = 0;
float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
Key key = row.mKeys.get(keyIndex);
key.width *= scaleFactor;
key.x = x;
x += key.width + key.gap;
}
}
}
mTotalWidth = newWidth;
}
}
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;
}
public int getKeyHeight() {
return mDefaultHeight;
}
protected void setKeyHeight(int height) {
mDefaultHeight = height;
}
public int getKeyWidth() {
return Math.round(mDefaultWidth);
}
protected void setKeyWidth(int width) {
mDefaultWidth = width;
}
// SMM {
public int getKeyTextWeight() {
if(mKeyTextWeight < 1) {
mKeyTextWeight = 2; // default 2
}
return mKeyTextWeight;
}
public boolean getShiftCapsMode() {
return mDefaultShiftCapsMode;
}
public boolean getDisablePreview() {
return mDefaultDisablePreview;
}
// } SMM
protected void setShiftCaps(boolean value) {
mDefaultShiftCapsMode = value;
}
/**
* Returns the total height of the keyboard
* @return the total height of the keyboard
*/
public int getHeight() {
return mTotalHeight;
}
public int getMinWidth() {
return mTotalWidth;
}
public boolean setShifted(boolean shiftState) {
for (Key shiftKey : mShiftKeys) {
if (shiftKey != null) {
shiftKey.on = shiftState;
}
}
if (mShifted != shiftState) {
mShifted = shiftState;
return true;
}
return false;
}
public boolean isShifted() {
return mShifted;
}
/**
* @hide
*/
public int[] getShiftKeyIndices() {
return mShiftKeyIndices;
}
public int getShiftKeyIndex() {
return mShiftKeyIndices[0];
}
private void computeNearestNeighbors() {
// Round-up so we don't have any pixels outside the grid
mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
mGridNeighbors = new int[GRID_SIZE][];
int[] indices = new int[mKeys.size()];
final int gridWidth = GRID_WIDTH * mCellWidth;
final int gridHeight = GRID_HEIGHT * 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);
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) {
indices[count++] = i;
}
}
int [] cell = new int[count];
System.arraycopy(indices, 0, cell, 0, count);
mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (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) * GRID_WIDTH + (x / mCellWidth);
if (index < GRID_SIZE) {
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;
int x = 0;
int y = 0;
Key key = null;
Row currentRow = null;
Resources res = context.getResources();
boolean skipRow = false;
try {
int event;
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);
rows.add(currentRow);
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
if (skipRow) {
skipToEndOfRow(parser);
inRow = false;
}
} else if (TAG_KEY.equals(tag)) {
inKey = true;
key = createKeyFromXml(res, currentRow, x, y, parser);
mKeys.add(key);
if (key.codes[0] == KeyCodes.KEYCODE_SHIFT) {
// Find available shift key slot and put this shift key in it
for (int i = 0; i < mShiftKeys.length; i++) {
if (mShiftKeys[i] == null) {
mShiftKeys[i] = key;
mShiftKeyIndices[i] = mKeys.size()-1;
break;
}
}
mModifierKeys.add(key);
} else if (key.codes[0] == KeyCodes.KEYCODE_ALT) {
mModifierKeys.add(key);
}
currentRow.mKeys.add(key);
} else if (TAG_KEYBOARD.equals(tag)) {
parseKeyboardAttributes(res, parser);
}
} else if (event == XmlResourceParser.END_TAG) {
if (inKey) {
inKey = false;
x += key.gap + key.width;
if (x > mTotalWidth) {
mTotalWidth = x;
}
} else if (inRow) {
inRow = false;
y += currentRow.verticalGap;
y += currentRow.defaultHeight;
} else {
// TODO: error or extend?
}
}
}
} catch (Exception e) {
Log.e(TAG, "Parse error:" + e);
e.printStackTrace();
}
mTotalHeight = y - mDefaultVerticalGap;
if(DEBUG) {
DisplayMetrics dm = context.getResources().getDisplayMetrics();
//double srcWidthInches = Math.pow(dm.widthPixels/dm.xdpi,2);
double srcHeightInches = Math.pow(dm.heightPixels/dm.ydpi,2);
double totalHeightInches = Math.pow(mTotalHeight/dm.ydpi,2); // Total Height = 1.3in
Log.i(TAG, "Screen Height : " + dm.heightPixels + " pixels, " + srcHeightInches + " inches;"
+ " Keyboard Height : " + mTotalHeight + " pixels, " + totalHeightInches + " inches;");
float textSize = res.getDimensionPixelOffset(R.dimen.key_label_text_size);
Paint paint = new Paint();
paint.setTextSize(textSize);
Rect textBounds = new Rect();
paint.getTextBounds("H", 0, 1, textBounds);
Log.i(TAG, "Text Size : " + textSize + "; width = " + textBounds.width() + "; height = " + textBounds.height());
}
}
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 = getDimensionOrFractionInt(a,
R.styleable.Keyboard_keyHeight,
mDisplayHeight, 50);
mDefaultHorizontalGap = getDimensionOrFraction(a,
R.styleable.Keyboard_horizontalGap,
mDisplayWidth, 0);
mDefaultVerticalGap = getDimensionOrFractionInt(a,
R.styleable.Keyboard_verticalGap,
mDisplayHeight, 0);
mKeyTextWeight = a.getInt(R.styleable.Keyboard_keyTextWeight, 2); // SMM
mDefaultShiftCapsMode = a.getBoolean(R.styleable.Keyboard_shiftCapsMode, false); // SMM
mDefaultDisablePreview = a.getBoolean(R.styleable.Keyboard_disablePreview, false); // SMM
mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
a.recycle();
// SMM { // Height fixed
mMaxHeight = Math.round(res.getFraction(R.fraction.keyboard_max_height, mDisplayHeight, mDisplayHeight));
mMinHeight = Math.round(res.getFraction(R.fraction.keyboard_min_height, mDisplayHeight, mDisplayHeight));
mDefaultHeight = Math.max(mDefaultHeight, mMinHeight / 4);
mDefaultHeight = Math.min(mDefaultHeight, mMaxHeight / 4);
// } SMM
}
static int getDimensionOrFractionInt(TypedArray a, int index, int base, int defValue) {
return Math.round(getDimensionOrFraction(a, index, base, defValue));
}
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 a.getFraction(index, base, base, defValue);
}
return defValue;
}
}