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