1 /* 2 * Copyright (C) 2008-2009 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package android.inputmethodservice; 18 19 import org.xmlpull.v1.XmlPullParserException; 20 21 import android.annotation.XmlRes; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.content.res.XmlResourceParser; 26 import android.graphics.drawable.Drawable; 27 import android.text.TextUtils; 28 import android.util.Log; 29 import android.util.TypedValue; 30 import android.util.Xml; 31 import android.util.DisplayMetrics; 32 33 import java.io.IOException; 34 import java.util.ArrayList; 35 import java.util.List; 36 import 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 * <Keyboard 45 * android:keyWidth="%10p" 46 * android:keyHeight="50px" 47 * android:horizontalGap="2px" 48 * android:verticalGap="2px" > 49 * <Row android:keyWidth="32px" > 50 * <Key android:keyLabel="A" /> 51 * ... 52 * </Row> 53 * ... 54 * </Keyboard> 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 */ 61 public 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 */ 101 private Key[] mShiftKeys = { null, null }; 102 103 /** Key index for the shift key, if present */ 104 private int[] mShiftKeyIndices = {-1, -1}; 105 106 /** 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. */ 146 private static float SEARCH_DISTANCE = 1.8f; 147 148 private ArrayList<Row> rows = new ArrayList<Row>(); 149 150 /** 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; 170 171 ArrayList<Key> mKeys = new ArrayList<Key>(); 172 173 /** 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 Row(Keyboard parent)184 public Row(Keyboard parent) { 185 this.parent = parent; 186 } 187 Row(Resources res, Keyboard parent, XmlResourceParser parser)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; 265 266 /** 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. */ Key(Row parent)314 public Key(Row parent) { 315 keyboard = parent.parent; 316 height = parent.defaultHeight; 317 width = parent.defaultWidth; 318 gap = parent.defaultHorizontalGap; 319 edgeFlags = parent.rowEdgeFlags; 320 } 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 */ Key(Resources res, Row parent, int x, int y, XmlResourceParser parser)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 */ onPressed()400 public void onPressed() { 401 pressed = !pressed; 402 } 403 404 /** 405 * 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}. 413 * <li>{@link android.os.Build.VERSION#SDK_INT} is greater than 414 * {@link android.os.Build.VERSION_CODES#LOLLIPOP_MR1}. 415 * </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. 419 * @see #onPressed() 420 */ onReleased(boolean inside)421 public void onReleased(boolean inside) { 422 pressed = !pressed; 423 if (sticky && inside) { 424 on = !on; 425 } 426 } 427 parseCSV(String value)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 */ isInside(int x, int y)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 */ squaredDistanceFrom(int x, int y)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 */ getCurrentDrawableState()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 */ Keyboard(Context context, int xmlLayoutResId)521 public Keyboard(Context context, int xmlLayoutResId) { 522 this(context, xmlLayoutResId, 0); 523 } 524 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 */ Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, int height)534 public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId, int width, 535 int height) { 536 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 549 /** 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 */ Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId)556 public Keyboard(Context context, @XmlRes int xmlLayoutResId, int modeId) { 557 DisplayMetrics dm = context.getResources().getDisplayMetrics(); 558 mDisplayWidth = dm.widthPixels; 559 mDisplayHeight = dm.heightPixels; 560 //Log.v(TAG, "keyboard's display metrics:" + dm); 561 562 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 */ Keyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding)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; 612 key.label = String.valueOf(c); 613 key.codes = new int[] { c }; 614 column++; 615 x += key.width + key.gap; 616 mKeys.add(key); 617 row.mKeys.add(key); 618 if (x > mTotalWidth) { 619 mTotalWidth = x; 620 } 621 } 622 mTotalHeight = y + mDefaultHeight; 623 rows.add(row); 624 } 625 resize(int newWidth, int newHeight)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. 655 } 656 getKeys()657 public List<Key> getKeys() { 658 return mKeys; 659 } 660 getModifierKeys()661 public List<Key> getModifierKeys() { 662 return mModifierKeys; 663 } 664 getHorizontalGap()665 protected int getHorizontalGap() { 666 return mDefaultHorizontalGap; 667 } 668 setHorizontalGap(int gap)669 protected void setHorizontalGap(int gap) { 670 mDefaultHorizontalGap = gap; 671 } 672 getVerticalGap()673 protected int getVerticalGap() { 674 return mDefaultVerticalGap; 675 } 676 setVerticalGap(int gap)677 protected void setVerticalGap(int gap) { 678 mDefaultVerticalGap = gap; 679 } 680 getKeyHeight()681 protected int getKeyHeight() { 682 return mDefaultHeight; 683 } 684 setKeyHeight(int height)685 protected void setKeyHeight(int height) { 686 mDefaultHeight = height; 687 } 688 getKeyWidth()689 protected int getKeyWidth() { 690 return mDefaultWidth; 691 } 692 setKeyWidth(int width)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 */ getHeight()701 public int getHeight() { 702 return mTotalHeight; 703 } 704 getMinWidth()705 public int getMinWidth() { 706 return mTotalWidth; 707 } 708 setShifted(boolean shiftState)709 public boolean setShifted(boolean shiftState) { 710 for (Key shiftKey : mShiftKeys) { 711 if (shiftKey != null) { 712 shiftKey.on = shiftState; 713 } 714 } 715 if (mShifted != shiftState) { 716 mShifted = shiftState; 717 return true; 718 } 719 return false; 720 } 721 isShifted()722 public boolean isShifted() { 723 return mShifted; 724 } 725 726 /** 727 * @hide 728 */ getShiftKeyIndices()729 public int[] getShiftKeyIndices() { 730 return mShiftKeyIndices; 731 } 732 getShiftKeyIndex()733 public int getShiftKeyIndex() { 734 return mShiftKeyIndices[0]; 735 } 736 computeNearestNeighbors()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 */ getNearestKeys(int x, int y)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 createRowFromXml(Resources res, XmlResourceParser parser)783 protected Row createRowFromXml(Resources res, XmlResourceParser parser) { 784 return new Row(res, this, parser); 785 } 786 createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser)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 loadKeyboard(Context context, XmlResourceParser parser)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; 803 804 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); 813 rows.add(currentRow); 814 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) { 824 // 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 } 832 mModifierKeys.add(key); 833 } else if (key.codes[0] == KEYCODE_ALT) { 834 mModifierKeys.add(key); 835 } 836 currentRow.mKeys.add(key); 837 } 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 skipToEndOfRow(XmlResourceParser parser)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 parseKeyboardAttributes(Resources res, XmlResourceParser parser)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 getDimensionOrFraction(TypedArray a, int index, int base, int defValue)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 } 908