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