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