Home | History | Annotate | Download | only in inputmethodservice
      1 /*
      2  * Copyright (C) 2008-2009 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package android.inputmethodservice;
     18 
     19 import org.xmlpull.v1.XmlPullParserException;
     20 
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.content.res.TypedArray;
     24 import android.content.res.XmlResourceParser;
     25 import android.graphics.drawable.Drawable;
     26 import android.text.TextUtils;
     27 import android.util.Log;
     28 import android.util.TypedValue;
     29 import android.util.Xml;
     30 import android.util.DisplayMetrics;
     31 
     32 import java.io.IOException;
     33 import java.util.ArrayList;
     34 import java.util.List;
     35 import java.util.StringTokenizer;
     36 
     37 
     38 /**
     39  * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
     40  * consists of rows of keys.
     41  * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
     42  * <pre>
     43  * &lt;Keyboard
     44  *         android:keyWidth="%10p"
     45  *         android:keyHeight="50px"
     46  *         android:horizontalGap="2px"
     47  *         android:verticalGap="2px" &gt;
     48  *     &lt;Row android:keyWidth="32px" &gt;
     49  *         &lt;Key android:keyLabel="A" /&gt;
     50  *         ...
     51  *     &lt;/Row&gt;
     52  *     ...
     53  * &lt;/Keyboard&gt;
     54  * </pre>
     55  * @attr ref android.R.styleable#Keyboard_keyWidth
     56  * @attr ref android.R.styleable#Keyboard_keyHeight
     57  * @attr ref android.R.styleable#Keyboard_horizontalGap
     58  * @attr ref android.R.styleable#Keyboard_verticalGap
     59  */
     60 public class Keyboard {
     61 
     62     static final String TAG = "Keyboard";
     63 
     64     // Keyboard XML Tags
     65     private static final String TAG_KEYBOARD = "Keyboard";
     66     private static final String TAG_ROW = "Row";
     67     private static final String TAG_KEY = "Key";
     68 
     69     public static final int EDGE_LEFT = 0x01;
     70     public static final int EDGE_RIGHT = 0x02;
     71     public static final int EDGE_TOP = 0x04;
     72     public static final int EDGE_BOTTOM = 0x08;
     73 
     74     public static final int KEYCODE_SHIFT = -1;
     75     public static final int KEYCODE_MODE_CHANGE = -2;
     76     public static final int KEYCODE_CANCEL = -3;
     77     public static final int KEYCODE_DONE = -4;
     78     public static final int KEYCODE_DELETE = -5;
     79     public static final int KEYCODE_ALT = -6;
     80 
     81     /** Keyboard label **/
     82     private CharSequence mLabel;
     83 
     84     /** Horizontal gap default for all rows */
     85     private int mDefaultHorizontalGap;
     86 
     87     /** Default key width */
     88     private int mDefaultWidth;
     89 
     90     /** Default key height */
     91     private int mDefaultHeight;
     92 
     93     /** Default gap between rows */
     94     private int mDefaultVerticalGap;
     95 
     96     /** Is the keyboard in the shifted state */
     97     private boolean mShifted;
     98 
     99     /** Key instance for the shift key, if present */
    100     private Key[] mShiftKeys = { null, null };
    101 
    102     /** Key index for the shift key, if present */
    103     private int[] mShiftKeyIndices = {-1, -1};
    104 
    105     /** Current key width, while loading the keyboard */
    106     private int mKeyWidth;
    107 
    108     /** Current key height, while loading the keyboard */
    109     private int mKeyHeight;
    110 
    111     /** Total height of the keyboard, including the padding and keys */
    112     private int mTotalHeight;
    113 
    114     /**
    115      * Total width of the keyboard, including left side gaps and keys, but not any gaps on the
    116      * right side.
    117      */
    118     private int mTotalWidth;
    119 
    120     /** List of keys in this keyboard */
    121     private List<Key> mKeys;
    122 
    123     /** List of modifier keys such as Shift & Alt, if any */
    124     private List<Key> mModifierKeys;
    125 
    126     /** Width of the screen available to fit the keyboard */
    127     private int mDisplayWidth;
    128 
    129     /** Height of the screen */
    130     private int mDisplayHeight;
    131 
    132     /** Keyboard mode, or zero, if none.  */
    133     private int mKeyboardMode;
    134 
    135     // Variables for pre-computing nearest keys.
    136 
    137     private static final int GRID_WIDTH = 10;
    138     private static final int GRID_HEIGHT = 5;
    139     private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
    140     private int mCellWidth;
    141     private int mCellHeight;
    142     private int[][] mGridNeighbors;
    143     private int mProximityThreshold;
    144     /** Number of key widths from current touch point to search for nearest keys. */
    145     private static float SEARCH_DISTANCE = 1.8f;
    146 
    147     private ArrayList<Row> rows = new ArrayList<Row>();
    148 
    149     /**
    150      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
    151      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
    152      * defines.
    153      * @attr ref android.R.styleable#Keyboard_keyWidth
    154      * @attr ref android.R.styleable#Keyboard_keyHeight
    155      * @attr ref android.R.styleable#Keyboard_horizontalGap
    156      * @attr ref android.R.styleable#Keyboard_verticalGap
    157      * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags
    158      * @attr ref android.R.styleable#Keyboard_Row_keyboardMode
    159      */
    160     public static class Row {
    161         /** Default width of a key in this row. */
    162         public int defaultWidth;
    163         /** Default height of a key in this row. */
    164         public int defaultHeight;
    165         /** Default horizontal gap between keys in this row. */
    166         public int defaultHorizontalGap;
    167         /** Vertical gap following this row. */
    168         public int verticalGap;
    169 
    170         ArrayList<Key> mKeys = new ArrayList<Key>();
    171 
    172         /**
    173          * Edge flags for this row of keys. Possible values that can be assigned are
    174          * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
    175          */
    176         public int rowEdgeFlags;
    177 
    178         /** The keyboard mode for this row */
    179         public int mode;
    180 
    181         private Keyboard parent;
    182 
    183         public Row(Keyboard parent) {
    184             this.parent = parent;
    185         }
    186 
    187         public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
    188             this.parent = parent;
    189             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    190                     com.android.internal.R.styleable.Keyboard);
    191             defaultWidth = getDimensionOrFraction(a,
    192                     com.android.internal.R.styleable.Keyboard_keyWidth,
    193                     parent.mDisplayWidth, parent.mDefaultWidth);
    194             defaultHeight = getDimensionOrFraction(a,
    195                     com.android.internal.R.styleable.Keyboard_keyHeight,
    196                     parent.mDisplayHeight, parent.mDefaultHeight);
    197             defaultHorizontalGap = getDimensionOrFraction(a,
    198                     com.android.internal.R.styleable.Keyboard_horizontalGap,
    199                     parent.mDisplayWidth, parent.mDefaultHorizontalGap);
    200             verticalGap = getDimensionOrFraction(a,
    201                     com.android.internal.R.styleable.Keyboard_verticalGap,
    202                     parent.mDisplayHeight, parent.mDefaultVerticalGap);
    203             a.recycle();
    204             a = res.obtainAttributes(Xml.asAttributeSet(parser),
    205                     com.android.internal.R.styleable.Keyboard_Row);
    206             rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0);
    207             mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode,
    208                     0);
    209         }
    210     }
    211 
    212     /**
    213      * Class for describing the position and characteristics of a single key in the keyboard.
    214      *
    215      * @attr ref android.R.styleable#Keyboard_keyWidth
    216      * @attr ref android.R.styleable#Keyboard_keyHeight
    217      * @attr ref android.R.styleable#Keyboard_horizontalGap
    218      * @attr ref android.R.styleable#Keyboard_Key_codes
    219      * @attr ref android.R.styleable#Keyboard_Key_keyIcon
    220      * @attr ref android.R.styleable#Keyboard_Key_keyLabel
    221      * @attr ref android.R.styleable#Keyboard_Key_iconPreview
    222      * @attr ref android.R.styleable#Keyboard_Key_isSticky
    223      * @attr ref android.R.styleable#Keyboard_Key_isRepeatable
    224      * @attr ref android.R.styleable#Keyboard_Key_isModifier
    225      * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard
    226      * @attr ref android.R.styleable#Keyboard_Key_popupCharacters
    227      * @attr ref android.R.styleable#Keyboard_Key_keyOutputText
    228      * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags
    229      */
    230     public static class Key {
    231         /**
    232          * All the key codes (unicode or custom code) that this key could generate, zero'th
    233          * being the most important.
    234          */
    235         public int[] codes;
    236 
    237         /** Label to display */
    238         public CharSequence label;
    239 
    240         /** Icon to display instead of a label. Icon takes precedence over a label */
    241         public Drawable icon;
    242         /** Preview version of the icon, for the preview popup */
    243         public Drawable iconPreview;
    244         /** Width of the key, not including the gap */
    245         public int width;
    246         /** Height of the key, not including the gap */
    247         public int height;
    248         /** The horizontal gap before this key */
    249         public int gap;
    250         /** Whether this key is sticky, i.e., a toggle key */
    251         public boolean sticky;
    252         /** X coordinate of the key in the keyboard layout */
    253         public int x;
    254         /** Y coordinate of the key in the keyboard layout */
    255         public int y;
    256         /** The current pressed state of this key */
    257         public boolean pressed;
    258         /** If this is a sticky key, is it on? */
    259         public boolean on;
    260         /** Text to output when pressed. This can be multiple characters, like ".com" */
    261         public CharSequence text;
    262         /** Popup characters */
    263         public CharSequence popupCharacters;
    264 
    265         /**
    266          * Flags that specify the anchoring to edges of the keyboard for detecting touch events
    267          * that are just out of the boundary of the key. This is a bit mask of
    268          * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
    269          * {@link Keyboard#EDGE_BOTTOM}.
    270          */
    271         public int edgeFlags;
    272         /** Whether this is a modifier key, such as Shift or Alt */
    273         public boolean modifier;
    274         /** The keyboard that this key belongs to */
    275         private Keyboard keyboard;
    276         /**
    277          * If this key pops up a mini keyboard, this is the resource id for the XML layout for that
    278          * keyboard.
    279          */
    280         public int popupResId;
    281         /** Whether this key repeats itself when held down */
    282         public boolean repeatable;
    283 
    284 
    285         private final static int[] KEY_STATE_NORMAL_ON = {
    286             android.R.attr.state_checkable,
    287             android.R.attr.state_checked
    288         };
    289 
    290         private final static int[] KEY_STATE_PRESSED_ON = {
    291             android.R.attr.state_pressed,
    292             android.R.attr.state_checkable,
    293             android.R.attr.state_checked
    294         };
    295 
    296         private final static int[] KEY_STATE_NORMAL_OFF = {
    297             android.R.attr.state_checkable
    298         };
    299 
    300         private final static int[] KEY_STATE_PRESSED_OFF = {
    301             android.R.attr.state_pressed,
    302             android.R.attr.state_checkable
    303         };
    304 
    305         private final static int[] KEY_STATE_NORMAL = {
    306         };
    307 
    308         private final static int[] KEY_STATE_PRESSED = {
    309             android.R.attr.state_pressed
    310         };
    311 
    312         /** Create an empty key with no attributes. */
    313         public Key(Row parent) {
    314             keyboard = parent.parent;
    315             height = parent.defaultHeight;
    316             width = parent.defaultWidth;
    317             gap = parent.defaultHorizontalGap;
    318             edgeFlags = parent.rowEdgeFlags;
    319         }
    320 
    321         /** Create a key with the given top-left coordinate and extract its attributes from
    322          * the XML parser.
    323          * @param res resources associated with the caller's context
    324          * @param parent the row that this key belongs to. The row must already be attached to
    325          * a {@link Keyboard}.
    326          * @param x the x coordinate of the top-left
    327          * @param y the y coordinate of the top-left
    328          * @param parser the XML parser containing the attributes for this key
    329          */
    330         public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
    331             this(parent);
    332 
    333             this.x = x;
    334             this.y = y;
    335 
    336             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    337                     com.android.internal.R.styleable.Keyboard);
    338 
    339             width = getDimensionOrFraction(a,
    340                     com.android.internal.R.styleable.Keyboard_keyWidth,
    341                     keyboard.mDisplayWidth, parent.defaultWidth);
    342             height = getDimensionOrFraction(a,
    343                     com.android.internal.R.styleable.Keyboard_keyHeight,
    344                     keyboard.mDisplayHeight, parent.defaultHeight);
    345             gap = getDimensionOrFraction(a,
    346                     com.android.internal.R.styleable.Keyboard_horizontalGap,
    347                     keyboard.mDisplayWidth, parent.defaultHorizontalGap);
    348             a.recycle();
    349             a = res.obtainAttributes(Xml.asAttributeSet(parser),
    350                     com.android.internal.R.styleable.Keyboard_Key);
    351             this.x += gap;
    352             TypedValue codesValue = new TypedValue();
    353             a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes,
    354                     codesValue);
    355             if (codesValue.type == TypedValue.TYPE_INT_DEC
    356                     || codesValue.type == TypedValue.TYPE_INT_HEX) {
    357                 codes = new int[] { codesValue.data };
    358             } else if (codesValue.type == TypedValue.TYPE_STRING) {
    359                 codes = parseCSV(codesValue.string.toString());
    360             }
    361 
    362             iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview);
    363             if (iconPreview != null) {
    364                 iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
    365                         iconPreview.getIntrinsicHeight());
    366             }
    367             popupCharacters = a.getText(
    368                     com.android.internal.R.styleable.Keyboard_Key_popupCharacters);
    369             popupResId = a.getResourceId(
    370                     com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0);
    371             repeatable = a.getBoolean(
    372                     com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false);
    373             modifier = a.getBoolean(
    374                     com.android.internal.R.styleable.Keyboard_Key_isModifier, false);
    375             sticky = a.getBoolean(
    376                     com.android.internal.R.styleable.Keyboard_Key_isSticky, false);
    377             edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0);
    378             edgeFlags |= parent.rowEdgeFlags;
    379 
    380             icon = a.getDrawable(
    381                     com.android.internal.R.styleable.Keyboard_Key_keyIcon);
    382             if (icon != null) {
    383                 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
    384             }
    385             label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel);
    386             text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText);
    387 
    388             if (codes == null && !TextUtils.isEmpty(label)) {
    389                 codes = new int[] { label.charAt(0) };
    390             }
    391             a.recycle();
    392         }
    393 
    394         /**
    395          * Informs the key that it has been pressed, in case it needs to change its appearance or
    396          * state.
    397          * @see #onReleased(boolean)
    398          */
    399         public void onPressed() {
    400             pressed = !pressed;
    401         }
    402 
    403         /**
    404          * Changes the pressed state of the key. If it is a sticky key, it will also change the
    405          * toggled state of the key if the finger was release inside.
    406          * @param inside whether the finger was released inside the key
    407          * @see #onPressed()
    408          */
    409         public void onReleased(boolean inside) {
    410             pressed = !pressed;
    411             if (sticky) {
    412                 on = !on;
    413             }
    414         }
    415 
    416         int[] parseCSV(String value) {
    417             int count = 0;
    418             int lastIndex = 0;
    419             if (value.length() > 0) {
    420                 count++;
    421                 while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
    422                     count++;
    423                 }
    424             }
    425             int[] values = new int[count];
    426             count = 0;
    427             StringTokenizer st = new StringTokenizer(value, ",");
    428             while (st.hasMoreTokens()) {
    429                 try {
    430                     values[count++] = Integer.parseInt(st.nextToken());
    431                 } catch (NumberFormatException nfe) {
    432                     Log.e(TAG, "Error parsing keycodes " + value);
    433                 }
    434             }
    435             return values;
    436         }
    437 
    438         /**
    439          * Detects if a point falls inside this key.
    440          * @param x the x-coordinate of the point
    441          * @param y the y-coordinate of the point
    442          * @return whether or not the point falls inside the key. If the key is attached to an edge,
    443          * it will assume that all points between the key and the edge are considered to be inside
    444          * the key.
    445          */
    446         public boolean isInside(int x, int y) {
    447             boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
    448             boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
    449             boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
    450             boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
    451             if ((x >= this.x || (leftEdge && x <= this.x + this.width))
    452                     && (x < this.x + this.width || (rightEdge && x >= this.x))
    453                     && (y >= this.y || (topEdge && y <= this.y + this.height))
    454                     && (y < this.y + this.height || (bottomEdge && y >= this.y))) {
    455                 return true;
    456             } else {
    457                 return false;
    458             }
    459         }
    460 
    461         /**
    462          * Returns the square of the distance between the center of the key and the given point.
    463          * @param x the x-coordinate of the point
    464          * @param y the y-coordinate of the point
    465          * @return the square of the distance of the point from the center of the key
    466          */
    467         public int squaredDistanceFrom(int x, int y) {
    468             int xDist = this.x + width / 2 - x;
    469             int yDist = this.y + height / 2 - y;
    470             return xDist * xDist + yDist * yDist;
    471         }
    472 
    473         /**
    474          * Returns the drawable state for the key, based on the current state and type of the key.
    475          * @return the drawable state of the key.
    476          * @see android.graphics.drawable.StateListDrawable#setState(int[])
    477          */
    478         public int[] getCurrentDrawableState() {
    479             int[] states = KEY_STATE_NORMAL;
    480 
    481             if (on) {
    482                 if (pressed) {
    483                     states = KEY_STATE_PRESSED_ON;
    484                 } else {
    485                     states = KEY_STATE_NORMAL_ON;
    486                 }
    487             } else {
    488                 if (sticky) {
    489                     if (pressed) {
    490                         states = KEY_STATE_PRESSED_OFF;
    491                     } else {
    492                         states = KEY_STATE_NORMAL_OFF;
    493                     }
    494                 } else {
    495                     if (pressed) {
    496                         states = KEY_STATE_PRESSED;
    497                     }
    498                 }
    499             }
    500             return states;
    501         }
    502     }
    503 
    504     /**
    505      * Creates a keyboard from the given xml key layout file.
    506      * @param context the application or service context
    507      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
    508      */
    509     public Keyboard(Context context, int xmlLayoutResId) {
    510         this(context, xmlLayoutResId, 0);
    511     }
    512 
    513     /**
    514      * Creates a keyboard from the given xml key layout file. Weeds out rows
    515      * that have a keyboard mode defined but don't match the specified mode.
    516      * @param context the application or service context
    517      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
    518      * @param modeId keyboard mode identifier
    519      * @param width sets width of keyboard
    520      * @param height sets height of keyboard
    521      */
    522     public Keyboard(Context context, int xmlLayoutResId, int modeId, int width, int height) {
    523         mDisplayWidth = width;
    524         mDisplayHeight = height;
    525 
    526         mDefaultHorizontalGap = 0;
    527         mDefaultWidth = mDisplayWidth / 10;
    528         mDefaultVerticalGap = 0;
    529         mDefaultHeight = mDefaultWidth;
    530         mKeys = new ArrayList<Key>();
    531         mModifierKeys = new ArrayList<Key>();
    532         mKeyboardMode = modeId;
    533         loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
    534     }
    535 
    536     /**
    537      * Creates a keyboard from the given xml key layout file. Weeds out rows
    538      * that have a keyboard mode defined but don't match the specified mode.
    539      * @param context the application or service context
    540      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
    541      * @param modeId keyboard mode identifier
    542      */
    543     public Keyboard(Context context, int xmlLayoutResId, int modeId) {
    544         DisplayMetrics dm = context.getResources().getDisplayMetrics();
    545         mDisplayWidth = dm.widthPixels;
    546         mDisplayHeight = dm.heightPixels;
    547         //Log.v(TAG, "keyboard's display metrics:" + dm);
    548 
    549         mDefaultHorizontalGap = 0;
    550         mDefaultWidth = mDisplayWidth / 10;
    551         mDefaultVerticalGap = 0;
    552         mDefaultHeight = mDefaultWidth;
    553         mKeys = new ArrayList<Key>();
    554         mModifierKeys = new ArrayList<Key>();
    555         mKeyboardMode = modeId;
    556         loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
    557     }
    558 
    559     /**
    560      * <p>Creates a blank keyboard from the given resource file and populates it with the specified
    561      * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
    562      * </p>
    563      * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
    564      * possible in each row.</p>
    565      * @param context the application or service context
    566      * @param layoutTemplateResId the layout template file, containing no keys.
    567      * @param characters the list of characters to display on the keyboard. One key will be created
    568      * for each character.
    569      * @param columns the number of columns of keys to display. If this number is greater than the
    570      * number of keys that can fit in a row, it will be ignored. If this number is -1, the
    571      * keyboard will fit as many keys as possible in each row.
    572      */
    573     public Keyboard(Context context, int layoutTemplateResId,
    574             CharSequence characters, int columns, int horizontalPadding) {
    575         this(context, layoutTemplateResId);
    576         int x = 0;
    577         int y = 0;
    578         int column = 0;
    579         mTotalWidth = 0;
    580 
    581         Row row = new Row(this);
    582         row.defaultHeight = mDefaultHeight;
    583         row.defaultWidth = mDefaultWidth;
    584         row.defaultHorizontalGap = mDefaultHorizontalGap;
    585         row.verticalGap = mDefaultVerticalGap;
    586         row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
    587         final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
    588         for (int i = 0; i < characters.length(); i++) {
    589             char c = characters.charAt(i);
    590             if (column >= maxColumns
    591                     || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
    592                 x = 0;
    593                 y += mDefaultVerticalGap + mDefaultHeight;
    594                 column = 0;
    595             }
    596             final Key key = new Key(row);
    597             key.x = x;
    598             key.y = y;
    599             key.label = String.valueOf(c);
    600             key.codes = new int[] { c };
    601             column++;
    602             x += key.width + key.gap;
    603             mKeys.add(key);
    604             row.mKeys.add(key);
    605             if (x > mTotalWidth) {
    606                 mTotalWidth = x;
    607             }
    608         }
    609         mTotalHeight = y + mDefaultHeight;
    610         rows.add(row);
    611     }
    612 
    613     final void resize(int newWidth, int newHeight) {
    614         int numRows = rows.size();
    615         for (int rowIndex = 0; rowIndex < numRows; ++rowIndex) {
    616             Row row = rows.get(rowIndex);
    617             int numKeys = row.mKeys.size();
    618             int totalGap = 0;
    619             int totalWidth = 0;
    620             for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
    621                 Key key = row.mKeys.get(keyIndex);
    622                 if (keyIndex > 0) {
    623                     totalGap += key.gap;
    624                 }
    625                 totalWidth += key.width;
    626             }
    627             if (totalGap + totalWidth > newWidth) {
    628                 int x = 0;
    629                 float scaleFactor = (float)(newWidth - totalGap) / totalWidth;
    630                 for (int keyIndex = 0; keyIndex < numKeys; ++keyIndex) {
    631                     Key key = row.mKeys.get(keyIndex);
    632                     key.width *= scaleFactor;
    633                     key.x = x;
    634                     x += key.width + key.gap;
    635                 }
    636             }
    637         }
    638         mTotalWidth = newWidth;
    639         // TODO: This does not adjust the vertical placement according to the new size.
    640         // The main problem in the previous code was horizontal placement/size, but we should
    641         // also recalculate the vertical sizes/positions when we get this resize call.
    642     }
    643 
    644     public List<Key> getKeys() {
    645         return mKeys;
    646     }
    647 
    648     public List<Key> getModifierKeys() {
    649         return mModifierKeys;
    650     }
    651 
    652     protected int getHorizontalGap() {
    653         return mDefaultHorizontalGap;
    654     }
    655 
    656     protected void setHorizontalGap(int gap) {
    657         mDefaultHorizontalGap = gap;
    658     }
    659 
    660     protected int getVerticalGap() {
    661         return mDefaultVerticalGap;
    662     }
    663 
    664     protected void setVerticalGap(int gap) {
    665         mDefaultVerticalGap = gap;
    666     }
    667 
    668     protected int getKeyHeight() {
    669         return mDefaultHeight;
    670     }
    671 
    672     protected void setKeyHeight(int height) {
    673         mDefaultHeight = height;
    674     }
    675 
    676     protected int getKeyWidth() {
    677         return mDefaultWidth;
    678     }
    679 
    680     protected void setKeyWidth(int width) {
    681         mDefaultWidth = width;
    682     }
    683 
    684     /**
    685      * Returns the total height of the keyboard
    686      * @return the total height of the keyboard
    687      */
    688     public int getHeight() {
    689         return mTotalHeight;
    690     }
    691 
    692     public int getMinWidth() {
    693         return mTotalWidth;
    694     }
    695 
    696     public boolean setShifted(boolean shiftState) {
    697         for (Key shiftKey : mShiftKeys) {
    698             if (shiftKey != null) {
    699                 shiftKey.on = shiftState;
    700             }
    701         }
    702         if (mShifted != shiftState) {
    703             mShifted = shiftState;
    704             return true;
    705         }
    706         return false;
    707     }
    708 
    709     public boolean isShifted() {
    710         return mShifted;
    711     }
    712 
    713     /**
    714      * @hide
    715      */
    716     public int[] getShiftKeyIndices() {
    717         return mShiftKeyIndices;
    718     }
    719 
    720     public int getShiftKeyIndex() {
    721         return mShiftKeyIndices[0];
    722     }
    723 
    724     private void computeNearestNeighbors() {
    725         // Round-up so we don't have any pixels outside the grid
    726         mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
    727         mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
    728         mGridNeighbors = new int[GRID_SIZE][];
    729         int[] indices = new int[mKeys.size()];
    730         final int gridWidth = GRID_WIDTH * mCellWidth;
    731         final int gridHeight = GRID_HEIGHT * mCellHeight;
    732         for (int x = 0; x < gridWidth; x += mCellWidth) {
    733             for (int y = 0; y < gridHeight; y += mCellHeight) {
    734                 int count = 0;
    735                 for (int i = 0; i < mKeys.size(); i++) {
    736                     final Key key = mKeys.get(i);
    737                     if (key.squaredDistanceFrom(x, y) < mProximityThreshold ||
    738                             key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold ||
    739                             key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
    740                                 < mProximityThreshold ||
    741                             key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
    742                         indices[count++] = i;
    743                     }
    744                 }
    745                 int [] cell = new int[count];
    746                 System.arraycopy(indices, 0, cell, 0, count);
    747                 mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
    748             }
    749         }
    750     }
    751 
    752     /**
    753      * Returns the indices of the keys that are closest to the given point.
    754      * @param x the x-coordinate of the point
    755      * @param y the y-coordinate of the point
    756      * @return the array of integer indices for the nearest keys to the given point. If the given
    757      * point is out of range, then an array of size zero is returned.
    758      */
    759     public int[] getNearestKeys(int x, int y) {
    760         if (mGridNeighbors == null) computeNearestNeighbors();
    761         if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
    762             int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
    763             if (index < GRID_SIZE) {
    764                 return mGridNeighbors[index];
    765             }
    766         }
    767         return new int[0];
    768     }
    769 
    770     protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
    771         return new Row(res, this, parser);
    772     }
    773 
    774     protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
    775             XmlResourceParser parser) {
    776         return new Key(res, parent, x, y, parser);
    777     }
    778 
    779     private void loadKeyboard(Context context, XmlResourceParser parser) {
    780         boolean inKey = false;
    781         boolean inRow = false;
    782         boolean leftMostKey = false;
    783         int row = 0;
    784         int x = 0;
    785         int y = 0;
    786         Key key = null;
    787         Row currentRow = null;
    788         Resources res = context.getResources();
    789         boolean skipRow = false;
    790 
    791         try {
    792             int event;
    793             while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
    794                 if (event == XmlResourceParser.START_TAG) {
    795                     String tag = parser.getName();
    796                     if (TAG_ROW.equals(tag)) {
    797                         inRow = true;
    798                         x = 0;
    799                         currentRow = createRowFromXml(res, parser);
    800                         rows.add(currentRow);
    801                         skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
    802                         if (skipRow) {
    803                             skipToEndOfRow(parser);
    804                             inRow = false;
    805                         }
    806                    } else if (TAG_KEY.equals(tag)) {
    807                         inKey = true;
    808                         key = createKeyFromXml(res, currentRow, x, y, parser);
    809                         mKeys.add(key);
    810                         if (key.codes[0] == KEYCODE_SHIFT) {
    811                             // Find available shift key slot and put this shift key in it
    812                             for (int i = 0; i < mShiftKeys.length; i++) {
    813                                 if (mShiftKeys[i] == null) {
    814                                     mShiftKeys[i] = key;
    815                                     mShiftKeyIndices[i] = mKeys.size()-1;
    816                                     break;
    817                                 }
    818                             }
    819                             mModifierKeys.add(key);
    820                         } else if (key.codes[0] == KEYCODE_ALT) {
    821                             mModifierKeys.add(key);
    822                         }
    823                         currentRow.mKeys.add(key);
    824                     } else if (TAG_KEYBOARD.equals(tag)) {
    825                         parseKeyboardAttributes(res, parser);
    826                     }
    827                 } else if (event == XmlResourceParser.END_TAG) {
    828                     if (inKey) {
    829                         inKey = false;
    830                         x += key.gap + key.width;
    831                         if (x > mTotalWidth) {
    832                             mTotalWidth = x;
    833                         }
    834                     } else if (inRow) {
    835                         inRow = false;
    836                         y += currentRow.verticalGap;
    837                         y += currentRow.defaultHeight;
    838                         row++;
    839                     } else {
    840                         // TODO: error or extend?
    841                     }
    842                 }
    843             }
    844         } catch (Exception e) {
    845             Log.e(TAG, "Parse error:" + e);
    846             e.printStackTrace();
    847         }
    848         mTotalHeight = y - mDefaultVerticalGap;
    849     }
    850 
    851     private void skipToEndOfRow(XmlResourceParser parser)
    852             throws XmlPullParserException, IOException {
    853         int event;
    854         while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
    855             if (event == XmlResourceParser.END_TAG
    856                     && parser.getName().equals(TAG_ROW)) {
    857                 break;
    858             }
    859         }
    860     }
    861 
    862     private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
    863         TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    864                 com.android.internal.R.styleable.Keyboard);
    865 
    866         mDefaultWidth = getDimensionOrFraction(a,
    867                 com.android.internal.R.styleable.Keyboard_keyWidth,
    868                 mDisplayWidth, mDisplayWidth / 10);
    869         mDefaultHeight = getDimensionOrFraction(a,
    870                 com.android.internal.R.styleable.Keyboard_keyHeight,
    871                 mDisplayHeight, 50);
    872         mDefaultHorizontalGap = getDimensionOrFraction(a,
    873                 com.android.internal.R.styleable.Keyboard_horizontalGap,
    874                 mDisplayWidth, 0);
    875         mDefaultVerticalGap = getDimensionOrFraction(a,
    876                 com.android.internal.R.styleable.Keyboard_verticalGap,
    877                 mDisplayHeight, 0);
    878         mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
    879         mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
    880         a.recycle();
    881     }
    882 
    883     static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
    884         TypedValue value = a.peekValue(index);
    885         if (value == null) return defValue;
    886         if (value.type == TypedValue.TYPE_DIMENSION) {
    887             return a.getDimensionPixelOffset(index, defValue);
    888         } else if (value.type == TypedValue.TYPE_FRACTION) {
    889             // Round it to avoid values like 47.9999 from getting truncated
    890             return Math.round(a.getFraction(index, base, base, defValue));
    891         }
    892         return defValue;
    893     }
    894 }
    895