Keyboard.java revision ae09878698ba6ad56ea43843f5a0895c94c32d90
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.util.DisplayMetrics; 31 32import java.io.IOException; 33import java.util.ArrayList; 34import java.util.List; 35import java.util.StringTokenizer; 36 37 38/** 39 * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard 40 * consists of rows of keys. 41 * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> 42 * <pre> 43 * <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 */ 60public class Keyboard { 61 62 static final String TAG = "Keyboard"; 63 64 // Keyboard XML Tags 65 private static final String TAG_KEYBOARD = "Keyboard"; 66 private static final String TAG_ROW = "Row"; 67 private static final String TAG_KEY = "Key"; 68 69 public static final int EDGE_LEFT = 0x01; 70 public static final int EDGE_RIGHT = 0x02; 71 public static final int EDGE_TOP = 0x04; 72 public static final int EDGE_BOTTOM = 0x08; 73 74 public static final int KEYCODE_SHIFT = -1; 75 public static final int KEYCODE_MODE_CHANGE = -2; 76 public static final int KEYCODE_CANCEL = -3; 77 public static final int KEYCODE_DONE = -4; 78 public static final int KEYCODE_DELETE = -5; 79 public static final int KEYCODE_ALT = -6; 80 81 /** Keyboard label **/ 82 private CharSequence mLabel; 83 84 /** Horizontal gap default for all rows */ 85 private int mDefaultHorizontalGap; 86 87 /** Default key width */ 88 private int mDefaultWidth; 89 90 /** Default key height */ 91 private int mDefaultHeight; 92 93 /** Default gap between rows */ 94 private int mDefaultVerticalGap; 95 96 /** Is the keyboard in the shifted state */ 97 private boolean mShifted; 98 99 /** Key instance for the shift key, if present */ 100 private Key mShiftKey; 101 102 /** Key index for the shift key, if present */ 103 private int mShiftKeyIndex = -1; 104 105 /** Current key width, while loading the keyboard */ 106 private int mKeyWidth; 107 108 /** Current key height, while loading the keyboard */ 109 private int mKeyHeight; 110 111 /** Total height of the keyboard, including the padding and keys */ 112 private int mTotalHeight; 113 114 /** 115 * Total width of the keyboard, including left side gaps and keys, but not any gaps on the 116 * right side. 117 */ 118 private int mTotalWidth; 119 120 /** List of keys in this keyboard */ 121 private List<Key> mKeys; 122 123 /** List of modifier keys such as Shift & Alt, if any */ 124 private List<Key> mModifierKeys; 125 126 /** Width of the screen available to fit the keyboard */ 127 private int mDisplayWidth; 128 129 /** Height of the screen */ 130 private int mDisplayHeight; 131 132 /** Keyboard mode, or zero, if none. */ 133 private int mKeyboardMode; 134 135 // Variables for pre-computing nearest keys. 136 137 private static final int GRID_WIDTH = 10; 138 private static final int GRID_HEIGHT = 5; 139 private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT; 140 private int mCellWidth; 141 private int mCellHeight; 142 private int[][] mGridNeighbors; 143 private int mProximityThreshold; 144 /** Number of key widths from current touch point to search for nearest keys. */ 145 private static float SEARCH_DISTANCE = 1.8f; 146 147 /** 148 * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. 149 * Some of the key size defaults can be overridden per row from what the {@link Keyboard} 150 * defines. 151 * @attr ref android.R.styleable#Keyboard_keyWidth 152 * @attr ref android.R.styleable#Keyboard_keyHeight 153 * @attr ref android.R.styleable#Keyboard_horizontalGap 154 * @attr ref android.R.styleable#Keyboard_verticalGap 155 * @attr ref android.R.styleable#Keyboard_Row_rowEdgeFlags 156 * @attr ref android.R.styleable#Keyboard_Row_keyboardMode 157 */ 158 public static class Row { 159 /** Default width of a key in this row. */ 160 public int defaultWidth; 161 /** Default height of a key in this row. */ 162 public int defaultHeight; 163 /** Default horizontal gap between keys in this row. */ 164 public int defaultHorizontalGap; 165 /** Vertical gap following this row. */ 166 public int verticalGap; 167 /** 168 * Edge flags for this row of keys. Possible values that can be assigned are 169 * {@link Keyboard#EDGE_TOP EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM EDGE_BOTTOM} 170 */ 171 public int rowEdgeFlags; 172 173 /** The keyboard mode for this row */ 174 public int mode; 175 176 private Keyboard parent; 177 178 public Row(Keyboard parent) { 179 this.parent = parent; 180 } 181 182 public Row(Resources res, Keyboard parent, XmlResourceParser parser) { 183 this.parent = parent; 184 TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), 185 com.android.internal.R.styleable.Keyboard); 186 defaultWidth = getDimensionOrFraction(a, 187 com.android.internal.R.styleable.Keyboard_keyWidth, 188 parent.mDisplayWidth, parent.mDefaultWidth); 189 defaultHeight = getDimensionOrFraction(a, 190 com.android.internal.R.styleable.Keyboard_keyHeight, 191 parent.mDisplayHeight, parent.mDefaultHeight); 192 defaultHorizontalGap = getDimensionOrFraction(a, 193 com.android.internal.R.styleable.Keyboard_horizontalGap, 194 parent.mDisplayWidth, parent.mDefaultHorizontalGap); 195 verticalGap = getDimensionOrFraction(a, 196 com.android.internal.R.styleable.Keyboard_verticalGap, 197 parent.mDisplayHeight, parent.mDefaultVerticalGap); 198 a.recycle(); 199 a = res.obtainAttributes(Xml.asAttributeSet(parser), 200 com.android.internal.R.styleable.Keyboard_Row); 201 rowEdgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Row_rowEdgeFlags, 0); 202 mode = a.getResourceId(com.android.internal.R.styleable.Keyboard_Row_keyboardMode, 203 0); 204 } 205 } 206 207 /** 208 * Class for describing the position and characteristics of a single key in the keyboard. 209 * 210 * @attr ref android.R.styleable#Keyboard_keyWidth 211 * @attr ref android.R.styleable#Keyboard_keyHeight 212 * @attr ref android.R.styleable#Keyboard_horizontalGap 213 * @attr ref android.R.styleable#Keyboard_Key_codes 214 * @attr ref android.R.styleable#Keyboard_Key_keyIcon 215 * @attr ref android.R.styleable#Keyboard_Key_keyLabel 216 * @attr ref android.R.styleable#Keyboard_Key_iconPreview 217 * @attr ref android.R.styleable#Keyboard_Key_isSticky 218 * @attr ref android.R.styleable#Keyboard_Key_isRepeatable 219 * @attr ref android.R.styleable#Keyboard_Key_isModifier 220 * @attr ref android.R.styleable#Keyboard_Key_popupKeyboard 221 * @attr ref android.R.styleable#Keyboard_Key_popupCharacters 222 * @attr ref android.R.styleable#Keyboard_Key_keyOutputText 223 * @attr ref android.R.styleable#Keyboard_Key_keyEdgeFlags 224 */ 225 public static class Key { 226 /** 227 * All the key codes (unicode or custom code) that this key could generate, zero'th 228 * being the most important. 229 */ 230 public int[] codes; 231 232 /** Label to display */ 233 public CharSequence label; 234 235 /** Icon to display instead of a label. Icon takes precedence over a label */ 236 public Drawable icon; 237 /** Preview version of the icon, for the preview popup */ 238 public Drawable iconPreview; 239 /** Width of the key, not including the gap */ 240 public int width; 241 /** Height of the key, not including the gap */ 242 public int height; 243 /** The horizontal gap before this key */ 244 public int gap; 245 /** Whether this key is sticky, i.e., a toggle key */ 246 public boolean sticky; 247 /** X coordinate of the key in the keyboard layout */ 248 public int x; 249 /** Y coordinate of the key in the keyboard layout */ 250 public int y; 251 /** The current pressed state of this key */ 252 public boolean pressed; 253 /** If this is a sticky key, is it on? */ 254 public boolean on; 255 /** Text to output when pressed. This can be multiple characters, like ".com" */ 256 public CharSequence text; 257 /** Popup characters */ 258 public CharSequence popupCharacters; 259 260 /** 261 * Flags that specify the anchoring to edges of the keyboard for detecting touch events 262 * that are just out of the boundary of the key. This is a bit mask of 263 * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, {@link Keyboard#EDGE_TOP} and 264 * {@link Keyboard#EDGE_BOTTOM}. 265 */ 266 public int edgeFlags; 267 /** Whether this is a modifier key, such as Shift or Alt */ 268 public boolean modifier; 269 /** The keyboard that this key belongs to */ 270 private Keyboard keyboard; 271 /** 272 * If this key pops up a mini keyboard, this is the resource id for the XML layout for that 273 * keyboard. 274 */ 275 public int popupResId; 276 /** Whether this key repeats itself when held down */ 277 public boolean repeatable; 278 279 280 private final static int[] KEY_STATE_NORMAL_ON = { 281 android.R.attr.state_checkable, 282 android.R.attr.state_checked 283 }; 284 285 private final static int[] KEY_STATE_PRESSED_ON = { 286 android.R.attr.state_pressed, 287 android.R.attr.state_checkable, 288 android.R.attr.state_checked 289 }; 290 291 private final static int[] KEY_STATE_NORMAL_OFF = { 292 android.R.attr.state_checkable 293 }; 294 295 private final static int[] KEY_STATE_PRESSED_OFF = { 296 android.R.attr.state_pressed, 297 android.R.attr.state_checkable 298 }; 299 300 private final static int[] KEY_STATE_NORMAL = { 301 }; 302 303 private final static int[] KEY_STATE_PRESSED = { 304 android.R.attr.state_pressed 305 }; 306 307 /** Create an empty key with no attributes. */ 308 public Key(Row parent) { 309 keyboard = parent.parent; 310 } 311 312 /** Create a key with the given top-left coordinate and extract its attributes from 313 * the XML parser. 314 * @param res resources associated with the caller's context 315 * @param parent the row that this key belongs to. The row must already be attached to 316 * a {@link Keyboard}. 317 * @param x the x coordinate of the top-left 318 * @param y the y coordinate of the top-left 319 * @param parser the XML parser containing the attributes for this key 320 */ 321 public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { 322 this(parent); 323 324 this.x = x; 325 this.y = y; 326 327 TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), 328 com.android.internal.R.styleable.Keyboard); 329 330 width = getDimensionOrFraction(a, 331 com.android.internal.R.styleable.Keyboard_keyWidth, 332 keyboard.mDisplayWidth, parent.defaultWidth); 333 height = getDimensionOrFraction(a, 334 com.android.internal.R.styleable.Keyboard_keyHeight, 335 keyboard.mDisplayHeight, parent.defaultHeight); 336 gap = getDimensionOrFraction(a, 337 com.android.internal.R.styleable.Keyboard_horizontalGap, 338 keyboard.mDisplayWidth, parent.defaultHorizontalGap); 339 a.recycle(); 340 a = res.obtainAttributes(Xml.asAttributeSet(parser), 341 com.android.internal.R.styleable.Keyboard_Key); 342 this.x += gap; 343 TypedValue codesValue = new TypedValue(); 344 a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes, 345 codesValue); 346 if (codesValue.type == TypedValue.TYPE_INT_DEC 347 || codesValue.type == TypedValue.TYPE_INT_HEX) { 348 codes = new int[] { codesValue.data }; 349 } else if (codesValue.type == TypedValue.TYPE_STRING) { 350 codes = parseCSV(codesValue.string.toString()); 351 } 352 353 iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview); 354 if (iconPreview != null) { 355 iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), 356 iconPreview.getIntrinsicHeight()); 357 } 358 popupCharacters = a.getText( 359 com.android.internal.R.styleable.Keyboard_Key_popupCharacters); 360 popupResId = a.getResourceId( 361 com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); 362 repeatable = a.getBoolean( 363 com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); 364 modifier = a.getBoolean( 365 com.android.internal.R.styleable.Keyboard_Key_isModifier, false); 366 sticky = a.getBoolean( 367 com.android.internal.R.styleable.Keyboard_Key_isSticky, false); 368 edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); 369 edgeFlags |= parent.rowEdgeFlags; 370 371 icon = a.getDrawable( 372 com.android.internal.R.styleable.Keyboard_Key_keyIcon); 373 if (icon != null) { 374 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 375 } 376 label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); 377 text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); 378 379 if (codes == null && !TextUtils.isEmpty(label)) { 380 codes = new int[] { label.charAt(0) }; 381 } 382 a.recycle(); 383 } 384 385 /** 386 * Informs the key that it has been pressed, in case it needs to change its appearance or 387 * state. 388 * @see #onReleased(boolean) 389 */ 390 public void onPressed() { 391 pressed = !pressed; 392 } 393 394 /** 395 * Changes the pressed state of the key. If it is a sticky key, it will also change the 396 * toggled state of the key if the finger was release inside. 397 * @param inside whether the finger was released inside the key 398 * @see #onPressed() 399 */ 400 public void onReleased(boolean inside) { 401 pressed = !pressed; 402 if (sticky) { 403 on = !on; 404 } 405 } 406 407 int[] parseCSV(String value) { 408 int count = 0; 409 int lastIndex = 0; 410 if (value.length() > 0) { 411 count++; 412 while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { 413 count++; 414 } 415 } 416 int[] values = new int[count]; 417 count = 0; 418 StringTokenizer st = new StringTokenizer(value, ","); 419 while (st.hasMoreTokens()) { 420 try { 421 values[count++] = Integer.parseInt(st.nextToken()); 422 } catch (NumberFormatException nfe) { 423 Log.e(TAG, "Error parsing keycodes " + value); 424 } 425 } 426 return values; 427 } 428 429 /** 430 * Detects if a point falls inside this key. 431 * @param x the x-coordinate of the point 432 * @param y the y-coordinate of the point 433 * @return whether or not the point falls inside the key. If the key is attached to an edge, 434 * it will assume that all points between the key and the edge are considered to be inside 435 * the key. 436 */ 437 public boolean isInside(int x, int y) { 438 boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; 439 boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; 440 boolean topEdge = (edgeFlags & EDGE_TOP) > 0; 441 boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; 442 if ((x >= this.x || (leftEdge && x <= this.x + this.width)) 443 && (x < this.x + this.width || (rightEdge && x >= this.x)) 444 && (y >= this.y || (topEdge && y <= this.y + this.height)) 445 && (y < this.y + this.height || (bottomEdge && y >= this.y))) { 446 return true; 447 } else { 448 return false; 449 } 450 } 451 452 /** 453 * Returns the square of the distance between the center of the key and the given point. 454 * @param x the x-coordinate of the point 455 * @param y the y-coordinate of the point 456 * @return the square of the distance of the point from the center of the key 457 */ 458 public int squaredDistanceFrom(int x, int y) { 459 int xDist = this.x + width / 2 - x; 460 int yDist = this.y + height / 2 - y; 461 return xDist * xDist + yDist * yDist; 462 } 463 464 /** 465 * Returns the drawable state for the key, based on the current state and type of the key. 466 * @return the drawable state of the key. 467 * @see android.graphics.drawable.StateListDrawable#setState(int[]) 468 */ 469 public int[] getCurrentDrawableState() { 470 int[] states = KEY_STATE_NORMAL; 471 472 if (on) { 473 if (pressed) { 474 states = KEY_STATE_PRESSED_ON; 475 } else { 476 states = KEY_STATE_NORMAL_ON; 477 } 478 } else { 479 if (sticky) { 480 if (pressed) { 481 states = KEY_STATE_PRESSED_OFF; 482 } else { 483 states = KEY_STATE_NORMAL_OFF; 484 } 485 } else { 486 if (pressed) { 487 states = KEY_STATE_PRESSED; 488 } 489 } 490 } 491 return states; 492 } 493 } 494 495 /** 496 * Creates a keyboard from the given xml key layout file. 497 * @param context the application or service context 498 * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. 499 */ 500 public Keyboard(Context context, int xmlLayoutResId) { 501 this(context, xmlLayoutResId, 0); 502 } 503 504 /** 505 * Creates a keyboard from the given xml key layout file. Weeds out rows 506 * that have a keyboard mode defined but don't match the specified mode. 507 * @param context the application or service context 508 * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. 509 * @param modeId keyboard mode identifier 510 */ 511 public Keyboard(Context context, int xmlLayoutResId, int modeId) { 512 DisplayMetrics dm = context.getResources().getDisplayMetrics(); 513 mDisplayWidth = dm.widthPixels; 514 mDisplayHeight = dm.heightPixels; 515 //Log.v(TAG, "keyboard's display metrics:" + dm); 516 517 mDefaultHorizontalGap = 0; 518 mDefaultWidth = mDisplayWidth / 10; 519 mDefaultVerticalGap = 0; 520 mDefaultHeight = mDefaultWidth; 521 mKeys = new ArrayList<Key>(); 522 mModifierKeys = new ArrayList<Key>(); 523 mKeyboardMode = modeId; 524 loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); 525 } 526 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