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