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