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 mShiftKey;
    101 
    102     /** Key index for the shift key, if present */
    103     private int mShiftKeyIndex = -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     /**
    148      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
    149      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
    150      * defines.
    151      * @attr ref android.R.styleable#Keyboard_keyWidth
    152      * @attr ref android.R.styleable#Keyboard_keyHeight
    153      * @attr ref android.R.styleable#Keyboard_horizontalGap
    154      * @attr ref android.R.styleable#Keyboard_verticalGap
    155      * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags
    156      * @attr ref android.R.styleable#Keyboard_Row_keyboardMode
    157      */
    158     public static class Row {
    159         /** Default width of a key in this row. */
    160         public int defaultWidth;
    161         /** Default height of a key in this row. */
    162         public int defaultHeight;
    163         /** Default horizontal gap between keys in this row. */
    164         public int defaultHorizontalGap;
    165         /** Vertical gap following this row. */
    166         public int verticalGap;
    167         /**
    168          * Edge flags for this row of keys. Possible values that can be assigned are
    169          * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
    170          */
    171         public int rowEdgeFlags;
    172 
    173         /** The keyboard mode for this row */
    174         public int mode;
    175 
    176         private Keyboard parent;
    177 
    178         public Row(Keyboard parent) {
    179             this.parent = parent;
    180         }
    181 
    182         public Row(Resources res, Keyboard parent, XmlResourceParser parser) {
    183             this.parent = parent;
    184             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    185                     com.android.internal.R.styleable.Keyboard);
    186             defaultWidth = getDimensionOrFraction(a,
    187                     com.android.internal.R.styleable.Keyboard_keyWidth,
    188                     parent.mDisplayWidth, parent.mDefaultWidth);
    189             defaultHeight = getDimensionOrFraction(a,
    190                     com.android.internal.R.styleable.Keyboard_keyHeight,
    191                     parent.mDisplayHeight, parent.mDefaultHeight);
    192             defaultHorizontalGap = getDimensionOrFraction(a,
    193                     com.android.internal.R.styleable.Keyboard_horizontalGap,
    194                     parent.mDisplayWidth, parent.mDefaultHorizontalGap);
    195             verticalGap = getDimensionOrFraction(a,
    196                     com.android.internal.R.styleable.Keyboard_verticalGap,
    197                     parent.mDisplayHeight, parent.mDefaultVerticalGap);
    198             a.recycle();
    199             a = res.obtainAttributes(Xml.asAttributeSet(parser),
    200                     com.android.internal.R.styleable.Keyboard_Row);
    201             rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0);
    202             mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode,
    203                     0);
    204         }
    205     }
    206 
    207     /**
    208      * Class for describing the position and characteristics of a single key in the keyboard.
    209      *
    210      * @attr ref android.R.styleable#Keyboard_keyWidth
    211      * @attr ref android.R.styleable#Keyboard_keyHeight
    212      * @attr ref android.R.styleable#Keyboard_horizontalGap
    213      * @attr ref android.R.styleable#Keyboard_Key_codes
    214      * @attr ref android.R.styleable#Keyboard_Key_keyIcon
    215      * @attr ref android.R.styleable#Keyboard_Key_keyLabel
    216      * @attr ref android.R.styleable#Keyboard_Key_iconPreview
    217      * @attr ref android.R.styleable#Keyboard_Key_isSticky
    218      * @attr ref android.R.styleable#Keyboard_Key_isRepeatable
    219      * @attr ref android.R.styleable#Keyboard_Key_isModifier
    220      * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard
    221      * @attr ref android.R.styleable#Keyboard_Key_popupCharacters
    222      * @attr ref android.R.styleable#Keyboard_Key_keyOutputText
    223      * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags
    224      */
    225     public static class Key {
    226         /**
    227          * All the key codes (unicode or custom code) that this key could generate, zero'th
    228          * being the most important.
    229          */
    230         public int[] codes;
    231 
    232         /** Label to display */
    233         public CharSequence label;
    234 
    235         /** Icon to display instead of a label. Icon takes precedence over a label */
    236         public Drawable icon;
    237         /** Preview version of the icon, for the preview popup */
    238         public Drawable iconPreview;
    239         /** Width of the key, not including the gap */
    240         public int width;
    241         /** Height of the key, not including the gap */
    242         public int height;
    243         /** The horizontal gap before this key */
    244         public int gap;
    245         /** Whether this key is sticky, i.e., a toggle key */
    246         public boolean sticky;
    247         /** X coordinate of the key in the keyboard layout */
    248         public int x;
    249         /** Y coordinate of the key in the keyboard layout */
    250         public int y;
    251         /** The current pressed state of this key */
    252         public boolean pressed;
    253         /** If this is a sticky key, is it on? */
    254         public boolean on;
    255         /** Text to output when pressed. This can be multiple characters, like ".com" */
    256         public CharSequence text;
    257         /** Popup characters */
    258         public CharSequence popupCharacters;
    259 
    260         /**
    261          * Flags that specify the anchoring to edges of the keyboard for detecting touch events
    262          * that are just out of the boundary of the key. This is a bit mask of
    263          * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and
    264          * {@link Keyboard#EDGE_BOTTOM}.
    265          */
    266         public int edgeFlags;
    267         /** Whether this is a modifier key, such as Shift or Alt */
    268         public boolean modifier;
    269         /** The keyboard that this key belongs to */
    270         private Keyboard keyboard;
    271         /**
    272          * If this key pops up a mini keyboard, this is the resource id for the XML layout for that
    273          * keyboard.
    274          */
    275         public int popupResId;
    276         /** Whether this key repeats itself when held down */
    277         public boolean repeatable;
    278 
    279 
    280         private final static int[] KEY_STATE_NORMAL_ON = {
    281             android.R.attr.state_checkable,
    282             android.R.attr.state_checked
    283         };
    284 
    285         private final static int[] KEY_STATE_PRESSED_ON = {
    286             android.R.attr.state_pressed,
    287             android.R.attr.state_checkable,
    288             android.R.attr.state_checked
    289         };
    290 
    291         private final static int[] KEY_STATE_NORMAL_OFF = {
    292             android.R.attr.state_checkable
    293         };
    294 
    295         private final static int[] KEY_STATE_PRESSED_OFF = {
    296             android.R.attr.state_pressed,
    297             android.R.attr.state_checkable
    298         };
    299 
    300         private final static int[] KEY_STATE_NORMAL = {
    301         };
    302 
    303         private final static int[] KEY_STATE_PRESSED = {
    304             android.R.attr.state_pressed
    305         };
    306 
    307         /** Create an empty key with no attributes. */
    308         public Key(Row parent) {
    309             keyboard = parent.parent;
    310             height = parent.defaultHeight;
    311             width = parent.defaultWidth;
    312             gap = parent.defaultHorizontalGap;
    313             edgeFlags = parent.rowEdgeFlags;
    314         }
    315 
    316         /** Create a key with the given top-left coordinate and extract its attributes from
    317          * the XML parser.
    318          * @param res resources associated with the caller's context
    319          * @param parent the row that this key belongs to. The row must already be attached to
    320          * a {@link Keyboard}.
    321          * @param x the x coordinate of the top-left
    322          * @param y the y coordinate of the top-left
    323          * @param parser the XML parser containing the attributes for this key
    324          */
    325         public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) {
    326             this(parent);
    327 
    328             this.x = x;
    329             this.y = y;
    330 
    331             TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    332                     com.android.internal.R.styleable.Keyboard);
    333 
    334             width = getDimensionOrFraction(a,
    335                     com.android.internal.R.styleable.Keyboard_keyWidth,
    336                     keyboard.mDisplayWidth, parent.defaultWidth);
    337             height = getDimensionOrFraction(a,
    338                     com.android.internal.R.styleable.Keyboard_keyHeight,
    339                     keyboard.mDisplayHeight, parent.defaultHeight);
    340             gap = getDimensionOrFraction(a,
    341                     com.android.internal.R.styleable.Keyboard_horizontalGap,
    342                     keyboard.mDisplayWidth, parent.defaultHorizontalGap);
    343             a.recycle();
    344             a = res.obtainAttributes(Xml.asAttributeSet(parser),
    345                     com.android.internal.R.styleable.Keyboard_Key);
    346             this.x += gap;
    347             TypedValue codesValue = new TypedValue();
    348             a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes,
    349                     codesValue);
    350             if (codesValue.type == TypedValue.TYPE_INT_DEC
    351                     || codesValue.type == TypedValue.TYPE_INT_HEX) {
    352                 codes = new int[] { codesValue.data };
    353             } else if (codesValue.type == TypedValue.TYPE_STRING) {
    354                 codes = parseCSV(codesValue.string.toString());
    355             }
    356 
    357             iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview);
    358             if (iconPreview != null) {
    359                 iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(),
    360                         iconPreview.getIntrinsicHeight());
    361             }
    362             popupCharacters = a.getText(
    363                     com.android.internal.R.styleable.Keyboard_Key_popupCharacters);
    364             popupResId = a.getResourceId(
    365                     com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0);
    366             repeatable = a.getBoolean(
    367                     com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false);
    368             modifier = a.getBoolean(
    369                     com.android.internal.R.styleable.Keyboard_Key_isModifier, false);
    370             sticky = a.getBoolean(
    371                     com.android.internal.R.styleable.Keyboard_Key_isSticky, false);
    372             edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0);
    373             edgeFlags |= parent.rowEdgeFlags;
    374 
    375             icon = a.getDrawable(
    376                     com.android.internal.R.styleable.Keyboard_Key_keyIcon);
    377             if (icon != null) {
    378                 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
    379             }
    380             label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel);
    381             text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText);
    382 
    383             if (codes == null && !TextUtils.isEmpty(label)) {
    384                 codes = new int[] { label.charAt(0) };
    385             }
    386             a.recycle();
    387         }
    388 
    389         /**
    390          * Informs the key that it has been pressed, in case it needs to change its appearance or
    391          * state.
    392          * @see #onReleased(boolean)
    393          */
    394         public void onPressed() {
    395             pressed = !pressed;
    396         }
    397 
    398         /**
    399          * Changes the pressed state of the key. If it is a sticky key, it will also change the
    400          * toggled state of the key if the finger was release inside.
    401          * @param inside whether the finger was released inside the key
    402          * @see #onPressed()
    403          */
    404         public void onReleased(boolean inside) {
    405             pressed = !pressed;
    406             if (sticky) {
    407                 on = !on;
    408             }
    409         }
    410 
    411         int[] parseCSV(String value) {
    412             int count = 0;
    413             int lastIndex = 0;
    414             if (value.length() > 0) {
    415                 count++;
    416                 while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) {
    417                     count++;
    418                 }
    419             }
    420             int[] values = new int[count];
    421             count = 0;
    422             StringTokenizer st = new StringTokenizer(value, ",");
    423             while (st.hasMoreTokens()) {
    424                 try {
    425                     values[count++] = Integer.parseInt(st.nextToken());
    426                 } catch (NumberFormatException nfe) {
    427                     Log.e(TAG, "Error parsing keycodes " + value);
    428                 }
    429             }
    430             return values;
    431         }
    432 
    433         /**
    434          * Detects if a point falls inside this key.
    435          * @param x the x-coordinate of the point
    436          * @param y the y-coordinate of the point
    437          * @return whether or not the point falls inside the key. If the key is attached to an edge,
    438          * it will assume that all points between the key and the edge are considered to be inside
    439          * the key.
    440          */
    441         public boolean isInside(int x, int y) {
    442             boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0;
    443             boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0;
    444             boolean topEdge = (edgeFlags & EDGE_TOP) > 0;
    445             boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0;
    446             if ((x >= this.x || (leftEdge && x <= this.x + this.width))
    447                     && (x < this.x + this.width || (rightEdge && x >= this.x))
    448                     && (y >= this.y || (topEdge && y <= this.y + this.height))
    449                     && (y < this.y + this.height || (bottomEdge && y >= this.y))) {
    450                 return true;
    451             } else {
    452                 return false;
    453             }
    454         }
    455 
    456         /**
    457          * Returns the square of the distance between the center of the key and the given point.
    458          * @param x the x-coordinate of the point
    459          * @param y the y-coordinate of the point
    460          * @return the square of the distance of the point from the center of the key
    461          */
    462         public int squaredDistanceFrom(int x, int y) {
    463             int xDist = this.x + width / 2 - x;
    464             int yDist = this.y + height / 2 - y;
    465             return xDist * xDist + yDist * yDist;
    466         }
    467 
    468         /**
    469          * Returns the drawable state for the key, based on the current state and type of the key.
    470          * @return the drawable state of the key.
    471          * @see android.graphics.drawable.StateListDrawable#setState(int[])
    472          */
    473         public int[] getCurrentDrawableState() {
    474             int[] states = KEY_STATE_NORMAL;
    475 
    476             if (on) {
    477                 if (pressed) {
    478                     states = KEY_STATE_PRESSED_ON;
    479                 } else {
    480                     states = KEY_STATE_NORMAL_ON;
    481                 }
    482             } else {
    483                 if (sticky) {
    484                     if (pressed) {
    485                         states = KEY_STATE_PRESSED_OFF;
    486                     } else {
    487                         states = KEY_STATE_NORMAL_OFF;
    488                     }
    489                 } else {
    490                     if (pressed) {
    491                         states = KEY_STATE_PRESSED;
    492                     }
    493                 }
    494             }
    495             return states;
    496         }
    497     }
    498 
    499     /**
    500      * Creates a keyboard from the given xml key layout file.
    501      * @param context the application or service context
    502      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
    503      */
    504     public Keyboard(Context context, int xmlLayoutResId) {
    505         this(context, xmlLayoutResId, 0);
    506     }
    507 
    508     /**
    509      * Creates a keyboard from the given xml key layout file. Weeds out rows
    510      * that have a keyboard mode defined but don't match the specified mode.
    511      * @param context the application or service context
    512      * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
    513      * @param modeId keyboard mode identifier
    514      */
    515     public Keyboard(Context context, int xmlLayoutResId, int modeId) {
    516         DisplayMetrics dm = context.getResources().getDisplayMetrics();
    517         mDisplayWidth = dm.widthPixels;
    518         mDisplayHeight = dm.heightPixels;
    519         //Log.v(TAG, "keyboard's display metrics:" + dm);
    520 
    521         mDefaultHorizontalGap = 0;
    522         mDefaultWidth = mDisplayWidth / 10;
    523         mDefaultVerticalGap = 0;
    524         mDefaultHeight = mDefaultWidth;
    525         mKeys = new ArrayList<Key>();
    526         mModifierKeys = new ArrayList<Key>();
    527         mKeyboardMode = modeId;
    528         loadKeyboard(context, context.getResources().getXml(xmlLayoutResId));
    529     }
    530 
    531     /**
    532      * <p>Creates a blank keyboard from the given resource file and populates it with the specified
    533      * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
    534      * </p>
    535      * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
    536      * possible in each row.</p>
    537      * @param context the application or service context
    538      * @param layoutTemplateResId the layout template file, containing no keys.
    539      * @param characters the list of characters to display on the keyboard. One key will be created
    540      * for each character.
    541      * @param columns the number of columns of keys to display. If this number is greater than the
    542      * number of keys that can fit in a row, it will be ignored. If this number is -1, the
    543      * keyboard will fit as many keys as possible in each row.
    544      */
    545     public Keyboard(Context context, int layoutTemplateResId,
    546             CharSequence characters, int columns, int horizontalPadding) {
    547         this(context, layoutTemplateResId);
    548         int x = 0;
    549         int y = 0;
    550         int column = 0;
    551         mTotalWidth = 0;
    552 
    553         Row row = new Row(this);
    554         row.defaultHeight = mDefaultHeight;
    555         row.defaultWidth = mDefaultWidth;
    556         row.defaultHorizontalGap = mDefaultHorizontalGap;
    557         row.verticalGap = mDefaultVerticalGap;
    558         row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
    559         final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
    560         for (int i = 0; i < characters.length(); i++) {
    561             char c = characters.charAt(i);
    562             if (column >= maxColumns
    563                     || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
    564                 x = 0;
    565                 y += mDefaultVerticalGap + mDefaultHeight;
    566                 column = 0;
    567             }
    568             final Key key = new Key(row);
    569             key.x = x;
    570             key.y = y;
    571             key.label = String.valueOf(c);
    572             key.codes = new int[] { c };
    573             column++;
    574             x += key.width + key.gap;
    575             mKeys.add(key);
    576             if (x > mTotalWidth) {
    577                 mTotalWidth = x;
    578             }
    579         }
    580         mTotalHeight = y + mDefaultHeight;
    581     }
    582 
    583     public List<Key> getKeys() {
    584         return mKeys;
    585     }
    586 
    587     public List<Key> getModifierKeys() {
    588         return mModifierKeys;
    589     }
    590 
    591     protected int getHorizontalGap() {
    592         return mDefaultHorizontalGap;
    593     }
    594 
    595     protected void setHorizontalGap(int gap) {
    596         mDefaultHorizontalGap = gap;
    597     }
    598 
    599     protected int getVerticalGap() {
    600         return mDefaultVerticalGap;
    601     }
    602 
    603     protected void setVerticalGap(int gap) {
    604         mDefaultVerticalGap = gap;
    605     }
    606 
    607     protected int getKeyHeight() {
    608         return mDefaultHeight;
    609     }
    610 
    611     protected void setKeyHeight(int height) {
    612         mDefaultHeight = height;
    613     }
    614 
    615     protected int getKeyWidth() {
    616         return mDefaultWidth;
    617     }
    618 
    619     protected void setKeyWidth(int width) {
    620         mDefaultWidth = width;
    621     }
    622 
    623     /**
    624      * Returns the total height of the keyboard
    625      * @return the total height of the keyboard
    626      */
    627     public int getHeight() {
    628         return mTotalHeight;
    629     }
    630 
    631     public int getMinWidth() {
    632         return mTotalWidth;
    633     }
    634 
    635     public boolean setShifted(boolean shiftState) {
    636         if (mShiftKey != null) {
    637             mShiftKey.on = shiftState;
    638         }
    639         if (mShifted != shiftState) {
    640             mShifted = shiftState;
    641             return true;
    642         }
    643         return false;
    644     }
    645 
    646     public boolean isShifted() {
    647         return mShifted;
    648     }
    649 
    650     public int getShiftKeyIndex() {
    651         return mShiftKeyIndex;
    652     }
    653 
    654     private void computeNearestNeighbors() {
    655         // Round-up so we don't have any pixels outside the grid
    656         mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
    657         mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
    658         mGridNeighbors = new int[GRID_SIZE][];
    659         int[] indices = new int[mKeys.size()];
    660         final int gridWidth = GRID_WIDTH * mCellWidth;
    661         final int gridHeight = GRID_HEIGHT * mCellHeight;
    662         for (int x = 0; x < gridWidth; x += mCellWidth) {
    663             for (int y = 0; y < gridHeight; y += mCellHeight) {
    664                 int count = 0;
    665                 for (int i = 0; i < mKeys.size(); i++) {
    666                     final Key key = mKeys.get(i);
    667                     if (key.squaredDistanceFrom(x, y) < mProximityThreshold ||
    668                             key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold ||
    669                             key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
    670                                 < mProximityThreshold ||
    671                             key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) {
    672                         indices[count++] = i;
    673                     }
    674                 }
    675                 int [] cell = new int[count];
    676                 System.arraycopy(indices, 0, cell, 0, count);
    677                 mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
    678             }
    679         }
    680     }
    681 
    682     /**
    683      * Returns the indices of the keys that are closest to the given point.
    684      * @param x the x-coordinate of the point
    685      * @param y the y-coordinate of the point
    686      * @return the array of integer indices for the nearest keys to the given point. If the given
    687      * point is out of range, then an array of size zero is returned.
    688      */
    689     public int[] getNearestKeys(int x, int y) {
    690         if (mGridNeighbors == null) computeNearestNeighbors();
    691         if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
    692             int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
    693             if (index < GRID_SIZE) {
    694                 return mGridNeighbors[index];
    695             }
    696         }
    697         return new int[0];
    698     }
    699 
    700     protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
    701         return new Row(res, this, parser);
    702     }
    703 
    704     protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
    705             XmlResourceParser parser) {
    706         return new Key(res, parent, x, y, parser);
    707     }
    708 
    709     private void loadKeyboard(Context context, XmlResourceParser parser) {
    710         boolean inKey = false;
    711         boolean inRow = false;
    712         boolean leftMostKey = false;
    713         int row = 0;
    714         int x = 0;
    715         int y = 0;
    716         Key key = null;
    717         Row currentRow = null;
    718         Resources res = context.getResources();
    719         boolean skipRow = false;
    720 
    721         try {
    722             int event;
    723             while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
    724                 if (event == XmlResourceParser.START_TAG) {
    725                     String tag = parser.getName();
    726                     if (TAG_ROW.equals(tag)) {
    727                         inRow = true;
    728                         x = 0;
    729                         currentRow = createRowFromXml(res, parser);
    730                         skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
    731                         if (skipRow) {
    732                             skipToEndOfRow(parser);
    733                             inRow = false;
    734                         }
    735                    } else if (TAG_KEY.equals(tag)) {
    736                         inKey = true;
    737                         key = createKeyFromXml(res, currentRow, x, y, parser);
    738                         mKeys.add(key);
    739                         if (key.codes[0] == KEYCODE_SHIFT) {
    740                             mShiftKey = key;
    741                             mShiftKeyIndex = mKeys.size()-1;
    742                             mModifierKeys.add(key);
    743                         } else if (key.codes[0] == KEYCODE_ALT) {
    744                             mModifierKeys.add(key);
    745                         }
    746                     } else if (TAG_KEYBOARD.equals(tag)) {
    747                         parseKeyboardAttributes(res, parser);
    748                     }
    749                 } else if (event == XmlResourceParser.END_TAG) {
    750                     if (inKey) {
    751                         inKey = false;
    752                         x += key.gap + key.width;
    753                         if (x > mTotalWidth) {
    754                             mTotalWidth = x;
    755                         }
    756                     } else if (inRow) {
    757                         inRow = false;
    758                         y += currentRow.verticalGap;
    759                         y += currentRow.defaultHeight;
    760                         row++;
    761                     } else {
    762                         // TODO: error or extend?
    763                     }
    764                 }
    765             }
    766         } catch (Exception e) {
    767             Log.e(TAG, "Parse error:" + e);
    768             e.printStackTrace();
    769         }
    770         mTotalHeight = y - mDefaultVerticalGap;
    771     }
    772 
    773     private void skipToEndOfRow(XmlResourceParser parser)
    774             throws XmlPullParserException, IOException {
    775         int event;
    776         while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
    777             if (event == XmlResourceParser.END_TAG
    778                     && parser.getName().equals(TAG_ROW)) {
    779                 break;
    780             }
    781         }
    782     }
    783 
    784     private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) {
    785         TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
    786                 com.android.internal.R.styleable.Keyboard);
    787 
    788         mDefaultWidth = getDimensionOrFraction(a,
    789                 com.android.internal.R.styleable.Keyboard_keyWidth,
    790                 mDisplayWidth, mDisplayWidth / 10);
    791         mDefaultHeight = getDimensionOrFraction(a,
    792                 com.android.internal.R.styleable.Keyboard_keyHeight,
    793                 mDisplayHeight, 50);
    794         mDefaultHorizontalGap = getDimensionOrFraction(a,
    795                 com.android.internal.R.styleable.Keyboard_horizontalGap,
    796                 mDisplayWidth, 0);
    797         mDefaultVerticalGap = getDimensionOrFraction(a,
    798                 com.android.internal.R.styleable.Keyboard_verticalGap,
    799                 mDisplayHeight, 0);
    800         mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
    801         mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
    802         a.recycle();
    803     }
    804 
    805     static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) {
    806         TypedValue value = a.peekValue(index);
    807         if (value == null) return defValue;
    808         if (value.type == TypedValue.TYPE_DIMENSION) {
    809             return a.getDimensionPixelOffset(index, defValue);
    810         } else if (value.type == TypedValue.TYPE_FRACTION) {
    811             // Round it to avoid values like 47.9999 from getting truncated
    812             return Math.round(a.getFraction(index, base, base, defValue));
    813         }
    814         return defValue;
    815     }
    816 }
    817