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