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