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