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