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