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