blob: 3f25113874e87dc7eae16f99928902d89f8ed7ce [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
Mathew Inwood1dd7d112018-07-31 14:53:29 +010019import android.annotation.UnsupportedAppUsage;
Tor Norbye7b9c9122013-05-30 16:48:33 -070020import android.annotation.XmlRes;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.content.res.XmlResourceParser;
25import android.graphics.drawable.Drawable;
Mathew Inwood8c854f82018-09-14 12:35:36 +010026import android.os.Build;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.text.TextUtils;
Yohei Yukawab7a284e2018-12-13 15:53:16 -080028import android.util.DisplayMetrics;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.util.Log;
30import android.util.TypedValue;
31import android.util.Xml;
Yohei Yukawab7a284e2018-12-13 15:53:16 -080032
33import org.xmlpull.v1.XmlPullParserException;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034
35import java.io.IOException;
36import java.util.ArrayList;
37import java.util.List;
38import 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
Yohei Yukawab7a284e2018-12-13 15:53:16 -080062 * @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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066 */
Yohei Yukawab7a284e2018-12-13 15:53:16 -080067@Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080068public 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 */
Jim Miller6465f772011-01-19 22:01:25 -0800108 private Key[] mShiftKeys = { null, null };
109
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 /** Key index for the shift key, if present */
Jim Miller6465f772011-01-19 22:01:25 -0800111 private int[] mShiftKeyIndices = {-1, -1};
112
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113 /** 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 */
Mathew Inwood1dd7d112018-07-31 14:53:29 +0100120 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800121 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 */
Mathew Inwood1dd7d112018-07-31 14:53:29 +0100127 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800128 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 */
Mathew Inwood1dd7d112018-07-31 14:53:29 +0100134 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800135 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. */
Amith Yamasaniae098782009-08-13 13:00:12 -0700156 private static float SEARCH_DISTANCE = 1.8f;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800157
Chet Haasea95e1082011-09-19 16:21:53 -0700158 private ArrayList<Row> rows = new ArrayList<Row>();
159
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800160 /**
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;
Chet Haasea95e1082011-09-19 16:21:53 -0700180
181 ArrayList<Key> mKeys = new ArrayList<Key>();
182
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800183 /**
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
194 public Row(Keyboard parent) {
195 this.parent = parent;
196 }
197
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;
Chet Haasea95e1082011-09-19 16:21:53 -0700275
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 /**
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. */
324 public Key(Row parent) {
325 keyboard = parent.parent;
Tadashi G. Takaokab65b7cb2010-09-17 15:28:47 +0900326 height = parent.defaultHeight;
327 width = parent.defaultWidth;
328 gap = parent.defaultHorizontalGap;
329 edgeFlags = parent.rowEdgeFlags;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800330 }
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 */
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 */
410 public void onPressed() {
411 pressed = !pressed;
412 }
Yohei Yukawa5c31de32015-06-11 11:45:34 -0700413
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800414 /**
Yohei Yukawa5c31de32015-06-11 11:45:34 -0700415 * 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}.
Yohei Yukawa5b2a0982015-06-11 21:42:49 -0700423 * <li>{@link android.os.Build.VERSION#SDK_INT} is greater than
424 * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}.
Yohei Yukawa5c31de32015-06-11 11:45:34 -0700425 * </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.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800429 * @see #onPressed()
430 */
431 public void onReleased(boolean inside) {
432 pressed = !pressed;
Yohei Yukawa5c31de32015-06-11 11:45:34 -0700433 if (sticky && inside) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800434 on = !on;
435 }
436 }
437
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 */
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 */
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 */
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 */
531 public Keyboard(Context context, int xmlLayoutResId) {
532 this(context, xmlLayoutResId, 0);
533 }
Jae Yong Sung8171b512010-08-05 10:44:27 -0700534
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 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700544 public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width,
545 int height) {
Jae Yong Sung8171b512010-08-05 10:44:27 -0700546 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800559 /**
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 */
Tor Norbye7b9c9122013-05-30 16:48:33 -0700566 public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) {
Mitsuru Oshima58feea72009-05-11 15:54:27 -0700567 DisplayMetrics dm = context.getResources().getDisplayMetrics();
568 mDisplayWidth = dm.widthPixels;
569 mDisplayHeight = dm.heightPixels;
570 //Log.v(TAG, "keyboard's display metrics:" + dm);
571
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800572 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 */
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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800622 key.label = String.valueOf(c);
623 key.codes = new int[] { c };
624 column++;
625 x += key.width + key.gap;
626 mKeys.add(key);
Chet Haasea95e1082011-09-19 16:21:53 -0700627 row.mKeys.add(key);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800628 if (x > mTotalWidth) {
629 mTotalWidth = x;
630 }
631 }
Chet Haasea95e1082011-09-19 16:21:53 -0700632 mTotalHeight = y + mDefaultHeight;
633 rows.add(row);
634 }
635
Mathew Inwood8c854f82018-09-14 12:35:36 +0100636 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Chet Haasea95e1082011-09-19 16:21:53 -0700637 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.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800666 }
667
668 public List<Key> getKeys() {
669 return mKeys;
670 }
671
672 public List<Key> getModifierKeys() {
673 return mModifierKeys;
674 }
675
676 protected int getHorizontalGap() {
677 return mDefaultHorizontalGap;
678 }
679
680 protected void setHorizontalGap(int gap) {
681 mDefaultHorizontalGap = gap;
682 }
683
684 protected int getVerticalGap() {
685 return mDefaultVerticalGap;
686 }
687
688 protected void setVerticalGap(int gap) {
689 mDefaultVerticalGap = gap;
690 }
691
692 protected int getKeyHeight() {
693 return mDefaultHeight;
694 }
695
696 protected void setKeyHeight(int height) {
697 mDefaultHeight = height;
698 }
699
700 protected int getKeyWidth() {
701 return mDefaultWidth;
702 }
703
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 */
712 public int getHeight() {
713 return mTotalHeight;
714 }
715
716 public int getMinWidth() {
717 return mTotalWidth;
718 }
719
720 public boolean setShifted(boolean shiftState) {
Jim Miller6465f772011-01-19 22:01:25 -0800721 for (Key shiftKey : mShiftKeys) {
722 if (shiftKey != null) {
723 shiftKey.on = shiftState;
724 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800725 }
726 if (mShifted != shiftState) {
727 mShifted = shiftState;
728 return true;
729 }
730 return false;
731 }
732
733 public boolean isShifted() {
734 return mShifted;
735 }
736
Jim Miller6465f772011-01-19 22:01:25 -0800737 /**
738 * @hide
739 */
740 public int[] getShiftKeyIndices() {
741 return mShiftKeyIndices;
742 }
743
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800744 public int getShiftKeyIndex() {
Jim Miller6465f772011-01-19 22:01:25 -0800745 return mShiftKeyIndices[0];
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800746 }
747
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 */
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
794 protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
795 return new Row(res, this, parser);
796 }
797
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
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;
Chet Haasea95e1082011-09-19 16:21:53 -0700814
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800815 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);
Chet Haasea95e1082011-09-19 16:21:53 -0700824 rows.add(currentRow);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800825 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) {
Jim Miller6465f772011-01-19 22:01:25 -0800835 // 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800843 mModifierKeys.add(key);
844 } else if (key.codes[0] == KEYCODE_ALT) {
845 mModifierKeys.add(key);
846 }
Chet Haasea95e1082011-09-19 16:21:53 -0700847 currentRow.mKeys.add(key);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800848 } 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
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
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
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}