| /* |
| * 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 android.inputmethodservice; |
| |
| import android.annotation.XmlRes; |
| import android.compat.annotation.UnsupportedAppUsage; |
| 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.os.Build; |
| import android.text.TextUtils; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.util.Xml; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.List; |
| 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 |
| * @deprecated This class is deprecated because this is just a convenient UI widget class that |
| * application developers can re-implement on top of existing public APIs. If you have |
| * already depended on this class, consider copying the implementation from AOSP into |
| * your project or re-implementing a similar widget by yourselves |
| */ |
| @Deprecated |
| public class Keyboard { |
| |
| static final String TAG = "Keyboard"; |
| |
| // 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 = -6; |
| |
| /** Keyboard label **/ |
| private CharSequence mLabel; |
| |
| /** Horizontal gap default for all rows */ |
| private int mDefaultHorizontalGap; |
| |
| /** Default key width */ |
| private int mDefaultWidth; |
| |
| /** Default key height */ |
| private int mDefaultHeight; |
| |
| /** Default gap between rows */ |
| private int mDefaultVerticalGap; |
| |
| /** Is the keyboard in the shifted state */ |
| private boolean mShifted; |
| |
| /** Key instance for the shift key, if present */ |
| private Key[] mShiftKeys = { null, null }; |
| |
| /** Key index for the shift key, if present */ |
| private int[] mShiftKeyIndices = {-1, -1}; |
| |
| /** Current key width, while loading the keyboard */ |
| private int mKeyWidth; |
| |
| /** Current key height, while loading the keyboard */ |
| private int mKeyHeight; |
| |
| /** Total height of the keyboard, including the padding and keys */ |
| @UnsupportedAppUsage |
| private int mTotalHeight; |
| |
| /** |
| * Total width of the keyboard, including left side gaps and keys, but not any gaps on the |
| * right side. |
| */ |
| @UnsupportedAppUsage |
| private int mTotalWidth; |
| |
| /** List of keys in this keyboard */ |
| private List<Key> mKeys; |
| |
| /** List of modifier keys such as Shift & Alt, if any */ |
| @UnsupportedAppUsage |
| private List<Key> mModifierKeys; |
| |
| /** Width of the screen available to fit the keyboard */ |
| private int mDisplayWidth; |
| |
| /** Height of the screen */ |
| private int mDisplayHeight; |
| |
| /** Keyboard mode, or zero, if none. */ |
| private int mKeyboardMode; |
| |
| // 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 int defaultWidth; |
| /** Default height of a key in this row. */ |
| public int defaultHeight; |
| /** Default horizontal gap between keys in this row. */ |
| public int defaultHorizontalGap; |
| /** Vertical gap following this row. */ |
| public int verticalGap; |
| |
| 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), |
| com.android.internal.R.styleable.Keyboard); |
| defaultWidth = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyWidth, |
| parent.mDisplayWidth, parent.mDefaultWidth); |
| defaultHeight = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyHeight, |
| parent.mDisplayHeight, parent.mDefaultHeight); |
| defaultHorizontalGap = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_horizontalGap, |
| parent.mDisplayWidth, parent.mDefaultHorizontalGap); |
| verticalGap = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_verticalGap, |
| parent.mDisplayHeight, parent.mDefaultVerticalGap); |
| a.recycle(); |
| a = res.obtainAttributes(Xml.asAttributeSet(parser), |
| com.android.internal.R.styleable.Keyboard_Row); |
| rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0); |
| mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode, |
| 0); |
| 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 |
| * @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; |
| |
| /** Label to display */ |
| public CharSequence label; |
| |
| /** 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; |
| /** 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; |
| /** Text to output when pressed. This can be multiple characters, like ".com" */ |
| public CharSequence text; |
| /** Popup characters */ |
| public CharSequence popupCharacters; |
| |
| /** |
| * 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_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 = parent.defaultWidth; |
| gap = parent.defaultHorizontalGap; |
| edgeFlags = parent.rowEdgeFlags; |
| } |
| |
| /** 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), |
| com.android.internal.R.styleable.Keyboard); |
| |
| width = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyWidth, |
| keyboard.mDisplayWidth, parent.defaultWidth); |
| height = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyHeight, |
| keyboard.mDisplayHeight, parent.defaultHeight); |
| gap = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_horizontalGap, |
| keyboard.mDisplayWidth, parent.defaultHorizontalGap); |
| a.recycle(); |
| a = res.obtainAttributes(Xml.asAttributeSet(parser), |
| com.android.internal.R.styleable.Keyboard_Key); |
| this.x += gap; |
| TypedValue codesValue = new TypedValue(); |
| a.getValue(com.android.internal.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(com.android.internal.R.styleable.Keyboard_Key_iconPreview); |
| if (iconPreview != null) { |
| iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), |
| iconPreview.getIntrinsicHeight()); |
| } |
| popupCharacters = a.getText( |
| com.android.internal.R.styleable.Keyboard_Key_popupCharacters); |
| popupResId = a.getResourceId( |
| com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); |
| repeatable = a.getBoolean( |
| com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); |
| modifier = a.getBoolean( |
| com.android.internal.R.styleable.Keyboard_Key_isModifier, false); |
| sticky = a.getBoolean( |
| com.android.internal.R.styleable.Keyboard_Key_isSticky, false); |
| edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); |
| edgeFlags |= parent.rowEdgeFlags; |
| |
| icon = a.getDrawable( |
| com.android.internal.R.styleable.Keyboard_Key_keyIcon); |
| if (icon != null) { |
| icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); |
| } |
| label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); |
| text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); |
| |
| if (codes == null && !TextUtils.isEmpty(label)) { |
| codes = new int[] { label.charAt(0) }; |
| } |
| a.recycle(); |
| } |
| |
| /** |
| * 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. |
| * |
| * <p>Toggled state of the key will be flipped when all the following conditions are |
| * fulfilled:</p> |
| * |
| * <ul> |
| * <li>This is a sticky key, that is, {@link #sticky} is {@code true}. |
| * <li>The parameter {@code inside} is {@code true}. |
| * <li>{@link android.os.Build.VERSION#SDK_INT} is greater than |
| * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}. |
| * </ul> |
| * |
| * @param inside whether the finger was released inside the key. Works only on Android M and |
| * later. See the method document for details. |
| * @see #onPressed() |
| */ |
| public void onReleased(boolean inside) { |
| pressed = !pressed; |
| if (sticky && inside) { |
| 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 (sticky) { |
| if (pressed) { |
| states = KEY_STATE_PRESSED_OFF; |
| } else { |
| states = KEY_STATE_NORMAL_OFF; |
| } |
| } else { |
| if (pressed) { |
| states = KEY_STATE_PRESSED; |
| } |
| } |
| } |
| return states; |
| } |
| } |
| |
| /** |
| * 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, @XmlRes int xmlLayoutResId, int modeId, int width, |
| int height) { |
| mDisplayWidth = width; |
| mDisplayHeight = height; |
| |
| mDefaultHorizontalGap = 0; |
| mDefaultWidth = mDisplayWidth / 10; |
| mDefaultVerticalGap = 0; |
| mDefaultHeight = 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, @XmlRes int xmlLayoutResId, int modeId) { |
| DisplayMetrics dm = context.getResources().getDisplayMetrics(); |
| mDisplayWidth = dm.widthPixels; |
| mDisplayHeight = dm.heightPixels; |
| //Log.v(TAG, "keyboard's display metrics:" + dm); |
| |
| mDefaultHorizontalGap = 0; |
| mDefaultWidth = mDisplayWidth / 10; |
| mDefaultVerticalGap = 0; |
| mDefaultHeight = 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); |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) |
| final void resize(int newWidth, int newHeight) { |
| 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; |
| // TODO: This does not adjust the vertical placement according to the new size. |
| // The main problem in the previous code was horizontal placement/size, but we should |
| // also recalculate the vertical sizes/positions when we get this resize call. |
| } |
| |
| public List<Key> getKeys() { |
| return mKeys; |
| } |
| |
| public List<Key> getModifierKeys() { |
| return mModifierKeys; |
| } |
| |
| protected int getHorizontalGap() { |
| return 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 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 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; |
| boolean leftMostKey = false; |
| int row = 0; |
| 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] == 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] == 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; |
| row++; |
| } else { |
| // TODO: error or extend? |
| } |
| } |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Parse error:" + e); |
| e.printStackTrace(); |
| } |
| mTotalHeight = y - mDefaultVerticalGap; |
| } |
| |
| 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), |
| com.android.internal.R.styleable.Keyboard); |
| |
| mDefaultWidth = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyWidth, |
| mDisplayWidth, mDisplayWidth / 10); |
| mDefaultHeight = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_keyHeight, |
| mDisplayHeight, 50); |
| mDefaultHorizontalGap = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_horizontalGap, |
| mDisplayWidth, 0); |
| mDefaultVerticalGap = getDimensionOrFraction(a, |
| com.android.internal.R.styleable.Keyboard_verticalGap, |
| mDisplayHeight, 0); |
| mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE); |
| mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison |
| a.recycle(); |
| } |
| |
| static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { |
| TypedValue value = a.peekValue(index); |
| if (value == null) return defValue; |
| if (value.type == TypedValue.TYPE_DIMENSION) { |
| return a.getDimensionPixelOffset(index, 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 defValue; |
| } |
| } |