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