• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008-2009 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.inputmethodservice;
18 
19 import org.xmlpull.v1.XmlPullParserException;
20 
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.content.res.XmlResourceParser;
25 import android.graphics.drawable.Drawable;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.util.TypedValue;
29 import android.util.Xml;
30 import android.util.DisplayMetrics;
31 
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.StringTokenizer;
36 
37 
38 /**
39  * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
40  * consists of rows of keys.
41  * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
42  * <pre>
43  * &lt;Keyboard
44  *         android:keyWidth="%10p"
45  *         android:keyHeight="50px"
46  *         android:horizontalGap="2px"
47  *         android:verticalGap="2px" &gt;
48  *     &lt;Row android:keyWidth="32px" &gt;
49  *         &lt;Key android:keyLabel="A" /&gt;
50  *         ...
51  *     &lt;/Row&gt;
52  *     ...
53  * &lt;/Keyboard&gt;
54  * </pre>
55  * @attr ref android.R.styleable#Keyboard_keyWidth
56  * @attr ref android.R.styleable#Keyboard_keyHeight
57  * @attr ref android.R.styleable#Keyboard_horizontalGap
58  * @attr ref android.R.styleable#Keyboard_verticalGap
59  */
60 public class Keyboard {
61 
62     static final String TAG = "Keyboard";
63 
64     // Keyboard XML Tags
65     private static final String TAG_KEYBOARD = "Keyboard";
66     private static final String TAG_ROW = "Row";
67     private static final String TAG_KEY = "Key";
68 
69     public static final int EDGE_LEFT = 0x01;
70     public static final int EDGE_RIGHT = 0x02;
71     public static final int EDGE_TOP = 0x04;
72     public static final int EDGE_BOTTOM = 0x08;
73 
74     public static final int KEYCODE_SHIFT = -1;
75     public static final int KEYCODE_MODE_CHANGE = -2;
76     public static final int KEYCODE_CANCEL = -3;
77     public static final int KEYCODE_DONE = -4;
78     public static final int KEYCODE_DELETE = -5;
79     public static final int KEYCODE_ALT = -6;
80 
81     /** Keyboard label **/
82     private CharSequence mLabel;
83 
84     /** Horizontal gap default for all rows */
85     private int mDefaultHorizontalGap;
86 
87     /** Default key width */
88     private int mDefaultWidth;
89 
90     /** Default key height */
91     private int mDefaultHeight;
92 
93     /** Default gap between rows */
94     private int mDefaultVerticalGap;
95 
96     /** Is the keyboard in the shifted state */
97     private boolean mShifted;
98 
99     /** Key instance for the shift key, if present */
100     private Key mShiftKey;
101 
102     /** Key index for the shift key, if present */
103     private int mShiftKeyIndex = -1;
104 
105     /** Current key width, while loading the keyboard */
106     private int mKeyWidth;
107 
108     /** Current key height, while loading the keyboard */
109     private int mKeyHeight;
110 
111     /** Total height of the keyboard, including the padding and keys */
112     private int mTotalHeight;
113 
114     /**
115      * Total width of the keyboard, including left side gaps and keys, but not any gaps on the
116      * right side.
117      */
118     private int mTotalWidth;
119 
120     /** List of keys in this keyboard */
121     private List<Key> mKeys;
122 
123     /** List of modifier keys such as Shift & Alt, if any */
124     private List<Key> mModifierKeys;
125 
126     /** Width of the screen available to fit the keyboard */
127     private int mDisplayWidth;
128 
129     /** Height of the screen */
130     private int mDisplayHeight;
131 
132     /** Keyboard mode, or zero, if none.  */
133     private int mKeyboardMode;
134 
135     // Variables for pre-computing nearest keys.
136 
137     private static final int GRID_WIDTH = 10;
138     private static final int GRID_HEIGHT = 5;
139     private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
140     private int mCellWidth;
141     private int mCellHeight;
142     private int[][] mGridNeighbors;
143     private int mProximityThreshold;
144     /** Number of key widths from current touch point to search for nearest keys. */
145     private static float SEARCH_DISTANCE = 1.8f;
146 
147     /**
148      * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate.
149      * Some of the key size defaults can be overridden per row from what the {@link Keyboard}
150      * defines.
151      * @attr ref android.R.styleable#Keyboard_keyWidth
152      * @attr ref android.R.styleable#Keyboard_keyHeight
153      * @attr ref android.R.styleable#Keyboard_horizontalGap
154      * @attr ref android.R.styleable#Keyboard_verticalGap
155      * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags
156      * @attr ref android.R.styleable#Keyboard_Row_keyboardMode
157      */
158     public static class Row {
159         /** Default width of a key in this row. */
160         public int defaultWidth;
161         /** Default height of a key in this row. */
162         public int defaultHeight;
163         /** Default horizontal gap between keys in this row. */
164         public int defaultHorizontalGap;
165         /** Vertical gap following this row. */
166         public int verticalGap;
167         /**
168          * Edge flags for this row of keys. Possible values that can be assigned are
169          * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM}
170          */
171         public int rowEdgeFlags;
172 
173         /** The keyboard mode for this row */
174         public int mode;
175 
176         private Keyboard parent;
177 
Row(Keyboard parent)178         public Row(Keyboard parent) {
179             this.parent = parent;
180         }
181 
Row(Resources res, Keyboard parent, XmlResourceParser parser)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. */
Key(Row parent)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          */
Key(Resources res, Row parent, int x, int y, XmlResourceParser parser)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          */
onPressed()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          */
onReleased(boolean inside)404         public void onReleased(boolean inside) {
405             pressed = !pressed;
406             if (sticky) {
407                 on = !on;
408             }
409         }
410 
parseCSV(String value)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          */
isInside(int x, int y)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          */
squaredDistanceFrom(int x, int y)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          */
getCurrentDrawableState()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      */
Keyboard(Context context, int xmlLayoutResId)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      */
Keyboard(Context context, int xmlLayoutResId, int modeId)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      */
Keyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding)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 
getKeys()583     public List<Key> getKeys() {
584         return mKeys;
585     }
586 
getModifierKeys()587     public List<Key> getModifierKeys() {
588         return mModifierKeys;
589     }
590 
getHorizontalGap()591     protected int getHorizontalGap() {
592         return mDefaultHorizontalGap;
593     }
594 
setHorizontalGap(int gap)595     protected void setHorizontalGap(int gap) {
596         mDefaultHorizontalGap = gap;
597     }
598 
getVerticalGap()599     protected int getVerticalGap() {
600         return mDefaultVerticalGap;
601     }
602 
setVerticalGap(int gap)603     protected void setVerticalGap(int gap) {
604         mDefaultVerticalGap = gap;
605     }
606 
getKeyHeight()607     protected int getKeyHeight() {
608         return mDefaultHeight;
609     }
610 
setKeyHeight(int height)611     protected void setKeyHeight(int height) {
612         mDefaultHeight = height;
613     }
614 
getKeyWidth()615     protected int getKeyWidth() {
616         return mDefaultWidth;
617     }
618 
setKeyWidth(int width)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      */
getHeight()627     public int getHeight() {
628         return mTotalHeight;
629     }
630 
getMinWidth()631     public int getMinWidth() {
632         return mTotalWidth;
633     }
634 
setShifted(boolean shiftState)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 
isShifted()646     public boolean isShifted() {
647         return mShifted;
648     }
649 
getShiftKeyIndex()650     public int getShiftKeyIndex() {
651         return mShiftKeyIndex;
652     }
653 
computeNearestNeighbors()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      */
getNearestKeys(int x, int y)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 
createRowFromXml(Resources res, XmlResourceParser parser)700     protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
701         return new Row(res, this, parser);
702     }
703 
createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser)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 
loadKeyboard(Context context, XmlResourceParser parser)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 
skipToEndOfRow(XmlResourceParser parser)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 
parseKeyboardAttributes(Resources res, XmlResourceParser parser)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 
getDimensionOrFraction(TypedArray a, int index, int base, int defValue)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