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 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 Row(Keyboard parent)178 public Row(Keyboard parent) { 179 this.parent = parent; 180 } 181 Row(Resources res, Keyboard parent, XmlResourceParser parser)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. */ Key(Row parent)308 public Key(Row parent) { 309 keyboard = parent.parent; 310 height = parent.defaultHeight; 311 width = parent.defaultWidth; 312 gap = parent.defaultHorizontalGap; 313 edgeFlags = parent.rowEdgeFlags; 314 } 315 316 /** Create a key with the given top-left coordinate and extract its attributes from 317 * the XML parser. 318 * @param res resources associated with the caller's context 319 * @param parent the row that this key belongs to. The row must already be attached to 320 * a {@link Keyboard}. 321 * @param x the x coordinate of the top-left 322 * @param y the y coordinate of the top-left 323 * @param parser the XML parser containing the attributes for this key 324 */ Key(Resources res, Row parent, int x, int y, XmlResourceParser parser)325 public Key(Resources res, Row parent, int x, int y, XmlResourceParser parser) { 326 this(parent); 327 328 this.x = x; 329 this.y = y; 330 331 TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), 332 com.android.internal.R.styleable.Keyboard); 333 334 width = getDimensionOrFraction(a, 335 com.android.internal.R.styleable.Keyboard_keyWidth, 336 keyboard.mDisplayWidth, parent.defaultWidth); 337 height = getDimensionOrFraction(a, 338 com.android.internal.R.styleable.Keyboard_keyHeight, 339 keyboard.mDisplayHeight, parent.defaultHeight); 340 gap = getDimensionOrFraction(a, 341 com.android.internal.R.styleable.Keyboard_horizontalGap, 342 keyboard.mDisplayWidth, parent.defaultHorizontalGap); 343 a.recycle(); 344 a = res.obtainAttributes(Xml.asAttributeSet(parser), 345 com.android.internal.R.styleable.Keyboard_Key); 346 this.x += gap; 347 TypedValue codesValue = new TypedValue(); 348 a.getValue(com.android.internal.R.styleable.Keyboard_Key_codes, 349 codesValue); 350 if (codesValue.type == TypedValue.TYPE_INT_DEC 351 || codesValue.type == TypedValue.TYPE_INT_HEX) { 352 codes = new int[] { codesValue.data }; 353 } else if (codesValue.type == TypedValue.TYPE_STRING) { 354 codes = parseCSV(codesValue.string.toString()); 355 } 356 357 iconPreview = a.getDrawable(com.android.internal.R.styleable.Keyboard_Key_iconPreview); 358 if (iconPreview != null) { 359 iconPreview.setBounds(0, 0, iconPreview.getIntrinsicWidth(), 360 iconPreview.getIntrinsicHeight()); 361 } 362 popupCharacters = a.getText( 363 com.android.internal.R.styleable.Keyboard_Key_popupCharacters); 364 popupResId = a.getResourceId( 365 com.android.internal.R.styleable.Keyboard_Key_popupKeyboard, 0); 366 repeatable = a.getBoolean( 367 com.android.internal.R.styleable.Keyboard_Key_isRepeatable, false); 368 modifier = a.getBoolean( 369 com.android.internal.R.styleable.Keyboard_Key_isModifier, false); 370 sticky = a.getBoolean( 371 com.android.internal.R.styleable.Keyboard_Key_isSticky, false); 372 edgeFlags = a.getInt(com.android.internal.R.styleable.Keyboard_Key_keyEdgeFlags, 0); 373 edgeFlags |= parent.rowEdgeFlags; 374 375 icon = a.getDrawable( 376 com.android.internal.R.styleable.Keyboard_Key_keyIcon); 377 if (icon != null) { 378 icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 379 } 380 label = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyLabel); 381 text = a.getText(com.android.internal.R.styleable.Keyboard_Key_keyOutputText); 382 383 if (codes == null && !TextUtils.isEmpty(label)) { 384 codes = new int[] { label.charAt(0) }; 385 } 386 a.recycle(); 387 } 388 389 /** 390 * Informs the key that it has been pressed, in case it needs to change its appearance or 391 * state. 392 * @see #onReleased(boolean) 393 */ onPressed()394 public void onPressed() { 395 pressed = !pressed; 396 } 397 398 /** 399 * Changes the pressed state of the key. If it is a sticky key, it will also change the 400 * toggled state of the key if the finger was release inside. 401 * @param inside whether the finger was released inside the key 402 * @see #onPressed() 403 */ onReleased(boolean inside)404 public void onReleased(boolean inside) { 405 pressed = !pressed; 406 if (sticky) { 407 on = !on; 408 } 409 } 410 parseCSV(String value)411 int[] parseCSV(String value) { 412 int count = 0; 413 int lastIndex = 0; 414 if (value.length() > 0) { 415 count++; 416 while ((lastIndex = value.indexOf(",", lastIndex + 1)) > 0) { 417 count++; 418 } 419 } 420 int[] values = new int[count]; 421 count = 0; 422 StringTokenizer st = new StringTokenizer(value, ","); 423 while (st.hasMoreTokens()) { 424 try { 425 values[count++] = Integer.parseInt(st.nextToken()); 426 } catch (NumberFormatException nfe) { 427 Log.e(TAG, "Error parsing keycodes " + value); 428 } 429 } 430 return values; 431 } 432 433 /** 434 * Detects if a point falls inside this key. 435 * @param x the x-coordinate of the point 436 * @param y the y-coordinate of the point 437 * @return whether or not the point falls inside the key. If the key is attached to an edge, 438 * it will assume that all points between the key and the edge are considered to be inside 439 * the key. 440 */ isInside(int x, int y)441 public boolean isInside(int x, int y) { 442 boolean leftEdge = (edgeFlags & EDGE_LEFT) > 0; 443 boolean rightEdge = (edgeFlags & EDGE_RIGHT) > 0; 444 boolean topEdge = (edgeFlags & EDGE_TOP) > 0; 445 boolean bottomEdge = (edgeFlags & EDGE_BOTTOM) > 0; 446 if ((x >= this.x || (leftEdge && x <= this.x + this.width)) 447 && (x < this.x + this.width || (rightEdge && x >= this.x)) 448 && (y >= this.y || (topEdge && y <= this.y + this.height)) 449 && (y < this.y + this.height || (bottomEdge && y >= this.y))) { 450 return true; 451 } else { 452 return false; 453 } 454 } 455 456 /** 457 * Returns the square of the distance between the center of the key and the given point. 458 * @param x the x-coordinate of the point 459 * @param y the y-coordinate of the point 460 * @return the square of the distance of the point from the center of the key 461 */ squaredDistanceFrom(int x, int y)462 public int squaredDistanceFrom(int x, int y) { 463 int xDist = this.x + width / 2 - x; 464 int yDist = this.y + height / 2 - y; 465 return xDist * xDist + yDist * yDist; 466 } 467 468 /** 469 * Returns the drawable state for the key, based on the current state and type of the key. 470 * @return the drawable state of the key. 471 * @see android.graphics.drawable.StateListDrawable#setState(int[]) 472 */ getCurrentDrawableState()473 public int[] getCurrentDrawableState() { 474 int[] states = KEY_STATE_NORMAL; 475 476 if (on) { 477 if (pressed) { 478 states = KEY_STATE_PRESSED_ON; 479 } else { 480 states = KEY_STATE_NORMAL_ON; 481 } 482 } else { 483 if (sticky) { 484 if (pressed) { 485 states = KEY_STATE_PRESSED_OFF; 486 } else { 487 states = KEY_STATE_NORMAL_OFF; 488 } 489 } else { 490 if (pressed) { 491 states = KEY_STATE_PRESSED; 492 } 493 } 494 } 495 return states; 496 } 497 } 498 499 /** 500 * Creates a keyboard from the given xml key layout file. 501 * @param context the application or service context 502 * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. 503 */ Keyboard(Context context, int xmlLayoutResId)504 public Keyboard(Context context, int xmlLayoutResId) { 505 this(context, xmlLayoutResId, 0); 506 } 507 508 /** 509 * Creates a keyboard from the given xml key layout file. Weeds out rows 510 * that have a keyboard mode defined but don't match the specified mode. 511 * @param context the application or service context 512 * @param xmlLayoutResId the resource file that contains the keyboard layout and keys. 513 * @param modeId keyboard mode identifier 514 */ Keyboard(Context context, int xmlLayoutResId, int modeId)515 public Keyboard(Context context, int xmlLayoutResId, int modeId) { 516 DisplayMetrics dm = context.getResources().getDisplayMetrics(); 517 mDisplayWidth = dm.widthPixels; 518 mDisplayHeight = dm.heightPixels; 519 //Log.v(TAG, "keyboard's display metrics:" + dm); 520 521 mDefaultHorizontalGap = 0; 522 mDefaultWidth = mDisplayWidth / 10; 523 mDefaultVerticalGap = 0; 524 mDefaultHeight = mDefaultWidth; 525 mKeys = new ArrayList<Key>(); 526 mModifierKeys = new ArrayList<Key>(); 527 mKeyboardMode = modeId; 528 loadKeyboard(context, context.getResources().getXml(xmlLayoutResId)); 529 } 530 531 /** 532 * <p>Creates a blank keyboard from the given resource file and populates it with the specified 533 * characters in left-to-right, top-to-bottom fashion, using the specified number of columns. 534 * </p> 535 * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as 536 * possible in each row.</p> 537 * @param context the application or service context 538 * @param layoutTemplateResId the layout template file, containing no keys. 539 * @param characters the list of characters to display on the keyboard. One key will be created 540 * for each character. 541 * @param columns the number of columns of keys to display. If this number is greater than the 542 * number of keys that can fit in a row, it will be ignored. If this number is -1, the 543 * keyboard will fit as many keys as possible in each row. 544 */ Keyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding)545 public Keyboard(Context context, int layoutTemplateResId, 546 CharSequence characters, int columns, int horizontalPadding) { 547 this(context, layoutTemplateResId); 548 int x = 0; 549 int y = 0; 550 int column = 0; 551 mTotalWidth = 0; 552 553 Row row = new Row(this); 554 row.defaultHeight = mDefaultHeight; 555 row.defaultWidth = mDefaultWidth; 556 row.defaultHorizontalGap = mDefaultHorizontalGap; 557 row.verticalGap = mDefaultVerticalGap; 558 row.rowEdgeFlags = EDGE_TOP | EDGE_BOTTOM; 559 final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns; 560 for (int i = 0; i < characters.length(); i++) { 561 char c = characters.charAt(i); 562 if (column >= maxColumns 563 || x + mDefaultWidth + horizontalPadding > mDisplayWidth) { 564 x = 0; 565 y += mDefaultVerticalGap + mDefaultHeight; 566 column = 0; 567 } 568 final Key key = new Key(row); 569 key.x = x; 570 key.y = y; 571 key.label = String.valueOf(c); 572 key.codes = new int[] { c }; 573 column++; 574 x += key.width + key.gap; 575 mKeys.add(key); 576 if (x > mTotalWidth) { 577 mTotalWidth = x; 578 } 579 } 580 mTotalHeight = y + mDefaultHeight; 581 } 582 getKeys()583 public List<Key> getKeys() { 584 return mKeys; 585 } 586 getModifierKeys()587 public List<Key> getModifierKeys() { 588 return mModifierKeys; 589 } 590 getHorizontalGap()591 protected int getHorizontalGap() { 592 return mDefaultHorizontalGap; 593 } 594 setHorizontalGap(int gap)595 protected void setHorizontalGap(int gap) { 596 mDefaultHorizontalGap = gap; 597 } 598 getVerticalGap()599 protected int getVerticalGap() { 600 return mDefaultVerticalGap; 601 } 602 setVerticalGap(int gap)603 protected void setVerticalGap(int gap) { 604 mDefaultVerticalGap = gap; 605 } 606 getKeyHeight()607 protected int getKeyHeight() { 608 return mDefaultHeight; 609 } 610 setKeyHeight(int height)611 protected void setKeyHeight(int height) { 612 mDefaultHeight = height; 613 } 614 getKeyWidth()615 protected int getKeyWidth() { 616 return mDefaultWidth; 617 } 618 setKeyWidth(int width)619 protected void setKeyWidth(int width) { 620 mDefaultWidth = width; 621 } 622 623 /** 624 * Returns the total height of the keyboard 625 * @return the total height of the keyboard 626 */ getHeight()627 public int getHeight() { 628 return mTotalHeight; 629 } 630 getMinWidth()631 public int getMinWidth() { 632 return mTotalWidth; 633 } 634 setShifted(boolean shiftState)635 public boolean setShifted(boolean shiftState) { 636 if (mShiftKey != null) { 637 mShiftKey.on = shiftState; 638 } 639 if (mShifted != shiftState) { 640 mShifted = shiftState; 641 return true; 642 } 643 return false; 644 } 645 isShifted()646 public boolean isShifted() { 647 return mShifted; 648 } 649 getShiftKeyIndex()650 public int getShiftKeyIndex() { 651 return mShiftKeyIndex; 652 } 653 computeNearestNeighbors()654 private void computeNearestNeighbors() { 655 // Round-up so we don't have any pixels outside the grid 656 mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH; 657 mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT; 658 mGridNeighbors = new int[GRID_SIZE][]; 659 int[] indices = new int[mKeys.size()]; 660 final int gridWidth = GRID_WIDTH * mCellWidth; 661 final int gridHeight = GRID_HEIGHT * mCellHeight; 662 for (int x = 0; x < gridWidth; x += mCellWidth) { 663 for (int y = 0; y < gridHeight; y += mCellHeight) { 664 int count = 0; 665 for (int i = 0; i < mKeys.size(); i++) { 666 final Key key = mKeys.get(i); 667 if (key.squaredDistanceFrom(x, y) < mProximityThreshold || 668 key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold || 669 key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1) 670 < mProximityThreshold || 671 key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold) { 672 indices[count++] = i; 673 } 674 } 675 int [] cell = new int[count]; 676 System.arraycopy(indices, 0, cell, 0, count); 677 mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell; 678 } 679 } 680 } 681 682 /** 683 * Returns the indices of the keys that are closest to the given point. 684 * @param x the x-coordinate of the point 685 * @param y the y-coordinate of the point 686 * @return the array of integer indices for the nearest keys to the given point. If the given 687 * point is out of range, then an array of size zero is returned. 688 */ getNearestKeys(int x, int y)689 public int[] getNearestKeys(int x, int y) { 690 if (mGridNeighbors == null) computeNearestNeighbors(); 691 if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) { 692 int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth); 693 if (index < GRID_SIZE) { 694 return mGridNeighbors[index]; 695 } 696 } 697 return new int[0]; 698 } 699 createRowFromXml(Resources res, XmlResourceParser parser)700 protected Row createRowFromXml(Resources res, XmlResourceParser parser) { 701 return new Row(res, this, parser); 702 } 703 createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser)704 protected Key createKeyFromXml(Resources res, Row parent, int x, int y, 705 XmlResourceParser parser) { 706 return new Key(res, parent, x, y, parser); 707 } 708 loadKeyboard(Context context, XmlResourceParser parser)709 private void loadKeyboard(Context context, XmlResourceParser parser) { 710 boolean inKey = false; 711 boolean inRow = false; 712 boolean leftMostKey = false; 713 int row = 0; 714 int x = 0; 715 int y = 0; 716 Key key = null; 717 Row currentRow = null; 718 Resources res = context.getResources(); 719 boolean skipRow = false; 720 721 try { 722 int event; 723 while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { 724 if (event == XmlResourceParser.START_TAG) { 725 String tag = parser.getName(); 726 if (TAG_ROW.equals(tag)) { 727 inRow = true; 728 x = 0; 729 currentRow = createRowFromXml(res, parser); 730 skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode; 731 if (skipRow) { 732 skipToEndOfRow(parser); 733 inRow = false; 734 } 735 } else if (TAG_KEY.equals(tag)) { 736 inKey = true; 737 key = createKeyFromXml(res, currentRow, x, y, parser); 738 mKeys.add(key); 739 if (key.codes[0] == KEYCODE_SHIFT) { 740 mShiftKey = key; 741 mShiftKeyIndex = mKeys.size()-1; 742 mModifierKeys.add(key); 743 } else if (key.codes[0] == KEYCODE_ALT) { 744 mModifierKeys.add(key); 745 } 746 } else if (TAG_KEYBOARD.equals(tag)) { 747 parseKeyboardAttributes(res, parser); 748 } 749 } else if (event == XmlResourceParser.END_TAG) { 750 if (inKey) { 751 inKey = false; 752 x += key.gap + key.width; 753 if (x > mTotalWidth) { 754 mTotalWidth = x; 755 } 756 } else if (inRow) { 757 inRow = false; 758 y += currentRow.verticalGap; 759 y += currentRow.defaultHeight; 760 row++; 761 } else { 762 // TODO: error or extend? 763 } 764 } 765 } 766 } catch (Exception e) { 767 Log.e(TAG, "Parse error:" + e); 768 e.printStackTrace(); 769 } 770 mTotalHeight = y - mDefaultVerticalGap; 771 } 772 skipToEndOfRow(XmlResourceParser parser)773 private void skipToEndOfRow(XmlResourceParser parser) 774 throws XmlPullParserException, IOException { 775 int event; 776 while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) { 777 if (event == XmlResourceParser.END_TAG 778 && parser.getName().equals(TAG_ROW)) { 779 break; 780 } 781 } 782 } 783 parseKeyboardAttributes(Resources res, XmlResourceParser parser)784 private void parseKeyboardAttributes(Resources res, XmlResourceParser parser) { 785 TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser), 786 com.android.internal.R.styleable.Keyboard); 787 788 mDefaultWidth = getDimensionOrFraction(a, 789 com.android.internal.R.styleable.Keyboard_keyWidth, 790 mDisplayWidth, mDisplayWidth / 10); 791 mDefaultHeight = getDimensionOrFraction(a, 792 com.android.internal.R.styleable.Keyboard_keyHeight, 793 mDisplayHeight, 50); 794 mDefaultHorizontalGap = getDimensionOrFraction(a, 795 com.android.internal.R.styleable.Keyboard_horizontalGap, 796 mDisplayWidth, 0); 797 mDefaultVerticalGap = getDimensionOrFraction(a, 798 com.android.internal.R.styleable.Keyboard_verticalGap, 799 mDisplayHeight, 0); 800 mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE); 801 mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison 802 a.recycle(); 803 } 804 getDimensionOrFraction(TypedArray a, int index, int base, int defValue)805 static int getDimensionOrFraction(TypedArray a, int index, int base, int defValue) { 806 TypedValue value = a.peekValue(index); 807 if (value == null) return defValue; 808 if (value.type == TypedValue.TYPE_DIMENSION) { 809 return a.getDimensionPixelOffset(index, defValue); 810 } else if (value.type == TypedValue.TYPE_FRACTION) { 811 // Round it to avoid values like 47.9999 from getting truncated 812 return Math.round(a.getFraction(index, base, base, defValue)); 813 } 814 return defValue; 815 } 816 } 817