Keyboard.java revision 2419a31c98927da807710e8ea713aa8a98b8c3d7
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.keyboard; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.content.res.XmlResourceParser; 23import android.text.TextUtils; 24import android.util.DisplayMetrics; 25import android.util.Log; 26import android.util.TypedValue; 27import android.util.Xml; 28import android.view.InflateException; 29 30import com.android.inputmethod.compat.EditorInfoCompatUtils; 31import com.android.inputmethod.keyboard.internal.KeyStyles; 32import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 33import com.android.inputmethod.keyboard.internal.KeyboardShiftState; 34import com.android.inputmethod.latin.LatinImeLogger; 35import com.android.inputmethod.latin.R; 36import com.android.inputmethod.latin.XmlParseUtils; 37 38import org.xmlpull.v1.XmlPullParser; 39import org.xmlpull.v1.XmlPullParserException; 40 41import java.io.IOException; 42import java.util.Arrays; 43import java.util.Collections; 44import java.util.HashMap; 45import java.util.HashSet; 46import java.util.Map; 47import java.util.Set; 48 49/** 50 * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard 51 * consists of rows of keys. 52 * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p> 53 * <pre> 54 * <Keyboard 55 * latin:keyWidth="%10p" 56 * latin:keyHeight="50px" 57 * latin:horizontalGap="2px" 58 * latin:verticalGap="2px" > 59 * <Row latin:keyWidth="32px" > 60 * <Key latin:keyLabel="A" /> 61 * ... 62 * </Row> 63 * ... 64 * </Keyboard> 65 * </pre> 66 */ 67public class Keyboard { 68 private static final String TAG = Keyboard.class.getSimpleName(); 69 70 /** Some common keys code. Must be positive. 71 * These should be aligned with values/keycodes.xml 72 */ 73 public static final int CODE_ENTER = '\n'; 74 public static final int CODE_TAB = '\t'; 75 public static final int CODE_SPACE = ' '; 76 public static final int CODE_PERIOD = '.'; 77 public static final int CODE_DASH = '-'; 78 public static final int CODE_SINGLE_QUOTE = '\''; 79 public static final int CODE_DOUBLE_QUOTE = '"'; 80 // TODO: Check how this should work for right-to-left languages. It seems to stand 81 // that for rtl languages, a closing parenthesis is a left parenthesis. Is this 82 // managed by the font? Or is it a different char? 83 public static final int CODE_CLOSING_PARENTHESIS = ')'; 84 public static final int CODE_CLOSING_SQUARE_BRACKET = ']'; 85 public static final int CODE_CLOSING_CURLY_BRACKET = '}'; 86 public static final int CODE_CLOSING_ANGLE_BRACKET = '>'; 87 public static final int CODE_DIGIT0 = '0'; 88 public static final int CODE_PLUS = '+'; 89 private static final int MINIMUM_LETTER_CODE = CODE_TAB; 90 91 /** Special keys code. Must be negative. 92 * These should be aligned with values/keycodes.xml 93 */ 94 public static final int CODE_SHIFT = -1; 95 public static final int CODE_SWITCH_ALPHA_SYMBOL = -2; 96 public static final int CODE_CAPSLOCK = -3; 97 public static final int CODE_OUTPUT_TEXT = -4; 98 public static final int CODE_DELETE = -5; 99 public static final int CODE_SETTINGS = -6; 100 public static final int CODE_SHORTCUT = -7; 101 // Code value representing the code is not specified. 102 public static final int CODE_UNSPECIFIED = -9; 103 104 public final KeyboardId mId; 105 public final int mThemeId; 106 107 /** Total height of the keyboard, including the padding and keys */ 108 public final int mOccupiedHeight; 109 /** Total width of the keyboard, including the padding and keys */ 110 public final int mOccupiedWidth; 111 112 /** The padding above the keyboard */ 113 public final int mTopPadding; 114 /** Default gap between rows */ 115 public final int mVerticalGap; 116 117 public final int mMostCommonKeyHeight; 118 public final int mMostCommonKeyWidth; 119 120 /** More keys keyboard template */ 121 public final int mMoreKeysTemplate; 122 123 /** Maximum column for mini keyboard */ 124 public final int mMaxMiniKeyboardColumn; 125 126 /** True if Right-To-Left keyboard */ 127 public final boolean mIsRtlKeyboard; 128 129 /** List of keys and icons in this keyboard */ 130 public final Set<Key> mKeys; 131 public final Set<Key> mShiftKeys; 132 public final Set<Key> mShiftLockKeys; 133 public final KeyboardIconsSet mIconsSet; 134 135 private final Map<Integer, Key> mKeyCache = new HashMap<Integer, Key>(); 136 137 private final ProximityInfo mProximityInfo; 138 139 // TODO: Remove this variable. 140 private final KeyboardShiftState mShiftState = new KeyboardShiftState(); 141 142 public Keyboard(Params params) { 143 mId = params.mId; 144 mThemeId = params.mThemeId; 145 mOccupiedHeight = params.mOccupiedHeight; 146 mOccupiedWidth = params.mOccupiedWidth; 147 mMostCommonKeyHeight = params.mMostCommonKeyHeight; 148 mMostCommonKeyWidth = params.mMostCommonKeyWidth; 149 mIsRtlKeyboard = params.mIsRtlKeyboard; 150 mMoreKeysTemplate = params.mMoreKeysTemplate; 151 mMaxMiniKeyboardColumn = params.mMaxMiniKeyboardColumn; 152 153 mTopPadding = params.mTopPadding; 154 mVerticalGap = params.mVerticalGap; 155 156 mKeys = Collections.unmodifiableSet(params.mKeys); 157 mShiftKeys = Collections.unmodifiableSet(params.mShiftKeys); 158 mShiftLockKeys = Collections.unmodifiableSet(params.mShiftLockKeys); 159 mIconsSet = params.mIconsSet; 160 161 mProximityInfo = new ProximityInfo( 162 params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight, 163 mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection); 164 } 165 166 public ProximityInfo getProximityInfo() { 167 return mProximityInfo; 168 } 169 170 public Key getKey(int code) { 171 if (code == CODE_UNSPECIFIED) { 172 return null; 173 } 174 final Integer keyCode = code; 175 if (mKeyCache.containsKey(keyCode)) { 176 return mKeyCache.get(keyCode); 177 } 178 179 for (final Key key : mKeys) { 180 if (key.mCode == code) { 181 mKeyCache.put(keyCode, key); 182 return key; 183 } 184 } 185 mKeyCache.put(keyCode, null); 186 return null; 187 } 188 189 // TODO: Remove this method. 190 boolean hasShiftLockKey() { 191 return !mShiftLockKeys.isEmpty(); 192 } 193 194 // TODO: Remove this method. 195 void setShiftLocked(boolean newShiftLockState) { 196 for (final Key key : mShiftLockKeys) { 197 // To represent "shift locked" state. The highlight is handled by background image that 198 // might be a StateListDrawable. 199 key.setHighlightOn(newShiftLockState); 200 final int attrId = newShiftLockState 201 ? R.styleable.Keyboard_iconShiftKeyShifted 202 : R.styleable.Keyboard_iconShiftKey; 203 key.setIcon(mIconsSet.getIconByAttrId(attrId)); 204 } 205 mShiftState.setShiftLocked(newShiftLockState); 206 } 207 208 // TODO: Move this method to KeyboardId. 209 public boolean isShiftLocked() { 210 return mShiftState.isShiftLocked(); 211 } 212 213 private void setShiftKeyGraphics(boolean newShiftState) { 214 if (mShiftState.isShiftLocked()) { 215 return; 216 } 217 for (final Key key : mShiftKeys) { 218 final int attrId = newShiftState 219 ? R.styleable.Keyboard_iconShiftKeyShifted 220 : R.styleable.Keyboard_iconShiftKey; 221 key.setIcon(mIconsSet.getIconByAttrId(attrId)); 222 } 223 } 224 225 // TODO: Remove this method. 226 void setShifted(boolean newShiftState) { 227 setShiftKeyGraphics(newShiftState); 228 mShiftState.setShifted(newShiftState); 229 } 230 231 // TODO: Move this method to KeyboardId. 232 public boolean isShiftedOrShiftLocked() { 233 return mShiftState.isShiftedOrShiftLocked(); 234 } 235 236 // TODO: Remove this method 237 void setAutomaticTemporaryUpperCase() { 238 setShiftKeyGraphics(true); 239 mShiftState.setAutomaticTemporaryUpperCase(); 240 } 241 242 // TODO: Move this method to KeyboardId. 243 public boolean isManualTemporaryUpperCase() { 244 return mShiftState.isManualTemporaryUpperCase(); 245 } 246 247 // TODO: Remove this method. 248 public String adjustLabelCase(String label) { 249 if (mId.isAlphabetKeyboard() && isShiftedOrShiftLocked() && !TextUtils.isEmpty(label) 250 && label.length() < 3 && Character.isLowerCase(label.charAt(0))) { 251 return label.toUpperCase(mId.mLocale); 252 } 253 return label; 254 } 255 256 public static boolean isLetterCode(int code) { 257 return code >= MINIMUM_LETTER_CODE; 258 } 259 260 public static class Params { 261 public KeyboardId mId; 262 public int mThemeId; 263 264 /** Total height and width of the keyboard, including the paddings and keys */ 265 public int mOccupiedHeight; 266 public int mOccupiedWidth; 267 268 /** Base height and width of the keyboard used to calculate rows' or keys' heights and 269 * widths 270 */ 271 public int mBaseHeight; 272 public int mBaseWidth; 273 274 public int mTopPadding; 275 public int mBottomPadding; 276 public int mHorizontalEdgesPadding; 277 public int mHorizontalCenterPadding; 278 279 public int mDefaultRowHeight; 280 public int mDefaultKeyWidth; 281 public int mHorizontalGap; 282 public int mVerticalGap; 283 284 public boolean mIsRtlKeyboard; 285 public int mMoreKeysTemplate; 286 public int mMaxMiniKeyboardColumn; 287 288 public int GRID_WIDTH; 289 public int GRID_HEIGHT; 290 291 public final Set<Key> mKeys = new HashSet<Key>(); 292 public final Set<Key> mShiftKeys = new HashSet<Key>(); 293 public final Set<Key> mShiftLockKeys = new HashSet<Key>(); 294 public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet(); 295 296 public int mMostCommonKeyHeight = 0; 297 public int mMostCommonKeyWidth = 0; 298 299 public final TouchPositionCorrection mTouchPositionCorrection = 300 new TouchPositionCorrection(); 301 302 public static class TouchPositionCorrection { 303 private static final int TOUCH_POSITION_CORRECTION_RECORD_SIZE = 3; 304 305 public boolean mEnabled; 306 public float[] mXs; 307 public float[] mYs; 308 public float[] mRadii; 309 310 public void load(String[] data) { 311 final int dataLength = data.length; 312 if (dataLength % TOUCH_POSITION_CORRECTION_RECORD_SIZE != 0) { 313 if (LatinImeLogger.sDBG) 314 throw new RuntimeException( 315 "the size of touch position correction data is invalid"); 316 return; 317 } 318 319 final int length = dataLength / TOUCH_POSITION_CORRECTION_RECORD_SIZE; 320 mXs = new float[length]; 321 mYs = new float[length]; 322 mRadii = new float[length]; 323 try { 324 for (int i = 0; i < dataLength; ++i) { 325 final int type = i % TOUCH_POSITION_CORRECTION_RECORD_SIZE; 326 final int index = i / TOUCH_POSITION_CORRECTION_RECORD_SIZE; 327 final float value = Float.parseFloat(data[i]); 328 if (type == 0) { 329 mXs[index] = value; 330 } else if (type == 1) { 331 mYs[index] = value; 332 } else { 333 mRadii[index] = value; 334 } 335 } 336 } catch (NumberFormatException e) { 337 if (LatinImeLogger.sDBG) { 338 throw new RuntimeException( 339 "the number format for touch position correction data is invalid"); 340 } 341 mXs = null; 342 mYs = null; 343 mRadii = null; 344 } 345 } 346 347 public void setEnabled(boolean enabled) { 348 mEnabled = enabled; 349 } 350 351 public boolean isValid() { 352 return mEnabled && mXs != null && mYs != null && mRadii != null 353 && mXs.length > 0 && mYs.length > 0 && mRadii.length > 0; 354 } 355 } 356 357 protected void clearKeys() { 358 mKeys.clear(); 359 mShiftKeys.clear(); 360 mShiftLockKeys.clear(); 361 clearHistogram(); 362 } 363 364 public void onAddKey(Key key) { 365 mKeys.add(key); 366 updateHistogram(key); 367 if (key.mCode == Keyboard.CODE_SHIFT) { 368 mShiftKeys.add(key); 369 if (key.isSticky()) { 370 mShiftLockKeys.add(key); 371 } 372 } 373 } 374 375 private int mMaxHeightCount = 0; 376 private int mMaxWidthCount = 0; 377 private final Map<Integer, Integer> mHeightHistogram = new HashMap<Integer, Integer>(); 378 private final Map<Integer, Integer> mWidthHistogram = new HashMap<Integer, Integer>(); 379 380 private void clearHistogram() { 381 mMostCommonKeyHeight = 0; 382 mMaxHeightCount = 0; 383 mHeightHistogram.clear(); 384 385 mMaxWidthCount = 0; 386 mMostCommonKeyWidth = 0; 387 mWidthHistogram.clear(); 388 } 389 390 private static int updateHistogramCounter(Map<Integer, Integer> histogram, Integer key) { 391 final int count = (histogram.containsKey(key) ? histogram.get(key) : 0) + 1; 392 histogram.put(key, count); 393 return count; 394 } 395 396 private void updateHistogram(Key key) { 397 final Integer height = key.mHeight + key.mVerticalGap; 398 final int heightCount = updateHistogramCounter(mHeightHistogram, height); 399 if (heightCount > mMaxHeightCount) { 400 mMaxHeightCount = heightCount; 401 mMostCommonKeyHeight = height; 402 } 403 404 final Integer width = key.mWidth + key.mHorizontalGap; 405 final int widthCount = updateHistogramCounter(mWidthHistogram, width); 406 if (widthCount > mMaxWidthCount) { 407 mMaxWidthCount = widthCount; 408 mMostCommonKeyWidth = width; 409 } 410 } 411 } 412 413 /** 414 * Returns the array of the keys that are closest to the given point. 415 * @param x the x-coordinate of the point 416 * @param y the y-coordinate of the point 417 * @return the array of the nearest keys to the given point. If the given 418 * point is out of range, then an array of size zero is returned. 419 */ 420 public Key[] getNearestKeys(int x, int y) { 421 // Avoid dead pixels at edges of the keyboard 422 final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1)); 423 final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1)); 424 return mProximityInfo.getNearestKeys(adjustedX, adjustedY); 425 } 426 427 public static String printableCode(int code) { 428 switch (code) { 429 case CODE_SHIFT: return "shift"; 430 case CODE_SWITCH_ALPHA_SYMBOL: return "symbol"; 431 case CODE_CAPSLOCK: return "capslock"; 432 case CODE_OUTPUT_TEXT: return "text"; 433 case CODE_DELETE: return "delete"; 434 case CODE_SHORTCUT: return "shortcut"; 435 case CODE_UNSPECIFIED: return "unspec"; 436 default: 437 if (code <= 0) Log.w(TAG, "Unknown non-positive key code=" + code); 438 if (code < CODE_SPACE) return String.format("'\\u%02x'", code); 439 if (code < 0x100) return String.format("'%c'", code); 440 return String.format("'\\u%04x'", code); 441 } 442 } 443 444 public static String toThemeName(int themeId) { 445 // This should be aligned with theme-*.xml resource files' themeId attribute. 446 switch (themeId) { 447 case 0: return "Basic"; 448 case 1: return "BasicHighContrast"; 449 case 5: return "IceCreamSandwich"; 450 case 6: return "Stone"; 451 case 7: return "StoneBold"; 452 case 8: return "GingerBread"; 453 default: return null; 454 } 455 } 456 457 /** 458 * Keyboard Building helper. 459 * 460 * This class parses Keyboard XML file and eventually build a Keyboard. 461 * The Keyboard XML file looks like: 462 * <pre> 463 * >!-- xml/keyboard.xml --< 464 * >Keyboard keyboard_attributes*< 465 * >!-- Keyboard Content --< 466 * >Row row_attributes*< 467 * >!-- Row Content --< 468 * >Key key_attributes* /< 469 * >Spacer horizontalGap="0.2in" /< 470 * >include keyboardLayout="@xml/other_keys"< 471 * ... 472 * >/Row< 473 * >include keyboardLayout="@xml/other_rows"< 474 * ... 475 * >/Keyboard< 476 * </pre> 477 * The XML file which is included in other file must have >merge< as root element, 478 * such as: 479 * <pre> 480 * >!-- xml/other_keys.xml --< 481 * >merge< 482 * >Key key_attributes* /< 483 * ... 484 * >/merge< 485 * </pre> 486 * and 487 * <pre> 488 * >!-- xml/other_rows.xml --< 489 * >merge< 490 * >Row row_attributes*< 491 * >Key key_attributes* /< 492 * >/Row< 493 * ... 494 * >/merge< 495 * </pre> 496 * You can also use switch-case-default tags to select Rows and Keys. 497 * <pre> 498 * >switch< 499 * >case case_attribute*< 500 * >!-- Any valid tags at switch position --< 501 * >/case< 502 * ... 503 * >default< 504 * >!-- Any valid tags at switch position --< 505 * >/default< 506 * >/switch< 507 * </pre> 508 * You can declare Key style and specify styles within Key tags. 509 * <pre> 510 * >switch< 511 * >case mode="email"< 512 * >key-style styleName="f1-key" parentStyle="modifier-key" 513 * keyLabel=".com" 514 * /< 515 * >/case< 516 * >case mode="url"< 517 * >key-style styleName="f1-key" parentStyle="modifier-key" 518 * keyLabel="http://" 519 * /< 520 * >/case< 521 * >/switch< 522 * ... 523 * >Key keyStyle="shift-key" ... /< 524 * </pre> 525 */ 526 527 public static class Builder<KP extends Params> { 528 private static final String TAG = Builder.class.getSimpleName(); 529 private static final boolean DEBUG = false; 530 531 // Keyboard XML Tags 532 private static final String TAG_KEYBOARD = "Keyboard"; 533 private static final String TAG_ROW = "Row"; 534 private static final String TAG_KEY = "Key"; 535 private static final String TAG_SPACER = "Spacer"; 536 private static final String TAG_INCLUDE = "include"; 537 private static final String TAG_MERGE = "merge"; 538 private static final String TAG_SWITCH = "switch"; 539 private static final String TAG_CASE = "case"; 540 private static final String TAG_DEFAULT = "default"; 541 public static final String TAG_KEY_STYLE = "key-style"; 542 543 private static final int DEFAULT_KEYBOARD_COLUMNS = 10; 544 private static final int DEFAULT_KEYBOARD_ROWS = 4; 545 546 protected final KP mParams; 547 protected final Context mContext; 548 protected final Resources mResources; 549 private final DisplayMetrics mDisplayMetrics; 550 551 private int mCurrentY = 0; 552 private Row mCurrentRow = null; 553 private boolean mLeftEdge; 554 private boolean mTopEdge; 555 private Key mRightEdgeKey = null; 556 private final KeyStyles mKeyStyles = new KeyStyles(); 557 558 /** 559 * Container for keys in the keyboard. All keys in a row are at the same Y-coordinate. 560 * Some of the key size defaults can be overridden per row from what the {@link Keyboard} 561 * defines. 562 */ 563 public static class Row { 564 // keyWidth enum constants 565 private static final int KEYWIDTH_NOT_ENUM = 0; 566 private static final int KEYWIDTH_FILL_RIGHT = -1; 567 private static final int KEYWIDTH_FILL_BOTH = -2; 568 569 private final Params mParams; 570 /** Default width of a key in this row. */ 571 public final float mDefaultKeyWidth; 572 /** Default height of a key in this row. */ 573 public final int mRowHeight; 574 575 private final int mCurrentY; 576 // Will be updated by {@link Key}'s constructor. 577 private float mCurrentX; 578 579 public Row(Resources res, Params params, XmlPullParser parser, int y) { 580 mParams = params; 581 TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 582 R.styleable.Keyboard); 583 mRowHeight = (int)Builder.getDimensionOrFraction(keyboardAttr, 584 R.styleable.Keyboard_rowHeight, 585 params.mBaseHeight, params.mDefaultRowHeight); 586 keyboardAttr.recycle(); 587 TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 588 R.styleable.Keyboard_Key); 589 mDefaultKeyWidth = Builder.getDimensionOrFraction(keyAttr, 590 R.styleable.Keyboard_Key_keyWidth, 591 params.mBaseWidth, params.mDefaultKeyWidth); 592 keyAttr.recycle(); 593 594 mCurrentY = y; 595 mCurrentX = 0.0f; 596 } 597 598 public void setXPos(float keyXPos) { 599 mCurrentX = keyXPos; 600 } 601 602 public void advanceXPos(float width) { 603 mCurrentX += width; 604 } 605 606 public int getKeyY() { 607 return mCurrentY; 608 } 609 610 public float getKeyX(TypedArray keyAttr) { 611 final int widthType = Builder.getEnumValue(keyAttr, 612 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); 613 if (widthType == KEYWIDTH_FILL_BOTH) { 614 // If keyWidth is fillBoth, the key width should start right after the nearest 615 // key on the left hand side. 616 return mCurrentX; 617 } 618 619 final int keyboardRightEdge = mParams.mOccupiedWidth 620 - mParams.mHorizontalEdgesPadding; 621 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) { 622 final float keyXPos = Builder.getDimensionOrFraction(keyAttr, 623 R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0); 624 if (keyXPos < 0) { 625 // If keyXPos is negative, the actual x-coordinate will be 626 // keyboardWidth + keyXPos. 627 // keyXPos shouldn't be less than mCurrentX because drawable area for this 628 // key starts at mCurrentX. Or, this key will overlaps the adjacent key on 629 // its left hand side. 630 return Math.max(keyXPos + keyboardRightEdge, mCurrentX); 631 } else { 632 return keyXPos + mParams.mHorizontalEdgesPadding; 633 } 634 } 635 return mCurrentX; 636 } 637 638 public float getKeyWidth(TypedArray keyAttr, float keyXPos) { 639 final int widthType = Builder.getEnumValue(keyAttr, 640 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM); 641 switch (widthType) { 642 case KEYWIDTH_FILL_RIGHT: 643 case KEYWIDTH_FILL_BOTH: 644 final int keyboardRightEdge = 645 mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding; 646 // If keyWidth is fillRight, the actual key width will be determined to fill 647 // out the area up to the right edge of the keyboard. 648 // If keyWidth is fillBoth, the actual key width will be determined to fill out 649 // the area between the nearest key on the left hand side and the right edge of 650 // the keyboard. 651 return keyboardRightEdge - keyXPos; 652 default: // KEYWIDTH_NOT_ENUM 653 return Builder.getDimensionOrFraction(keyAttr, 654 R.styleable.Keyboard_Key_keyWidth, 655 mParams.mBaseWidth, mDefaultKeyWidth); 656 } 657 } 658 } 659 660 public Builder(Context context, KP params) { 661 mContext = context; 662 final Resources res = context.getResources(); 663 mResources = res; 664 mDisplayMetrics = res.getDisplayMetrics(); 665 666 mParams = params; 667 668 setTouchPositionCorrectionData(context, params); 669 670 params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width); 671 params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height); 672 } 673 674 private static void setTouchPositionCorrectionData(Context context, Params params) { 675 final TypedArray a = context.obtainStyledAttributes( 676 null, R.styleable.Keyboard, R.attr.keyboardStyle, 0); 677 params.mThemeId = a.getInt(R.styleable.Keyboard_themeId, 0); 678 final int resourceId = a.getResourceId( 679 R.styleable.Keyboard_touchPositionCorrectionData, 0); 680 a.recycle(); 681 if (resourceId == 0) { 682 if (LatinImeLogger.sDBG) 683 Log.e(TAG, "touchPositionCorrectionData is not defined"); 684 return; 685 } 686 687 final String[] data = context.getResources().getStringArray(resourceId); 688 params.mTouchPositionCorrection.load(data); 689 } 690 691 public Builder<KP> load(int xmlId, KeyboardId id) { 692 mParams.mId = id; 693 final XmlResourceParser parser = mResources.getXml(xmlId); 694 try { 695 parseKeyboard(parser); 696 } catch (XmlPullParserException e) { 697 Log.w(TAG, "keyboard XML parse error: " + e); 698 throw new IllegalArgumentException(e); 699 } catch (IOException e) { 700 Log.w(TAG, "keyboard XML parse error: " + e); 701 throw new RuntimeException(e); 702 } finally { 703 parser.close(); 704 } 705 return this; 706 } 707 708 public void setTouchPositionCorrectionEnabled(boolean enabled) { 709 mParams.mTouchPositionCorrection.setEnabled(enabled); 710 } 711 712 public Keyboard build() { 713 return new Keyboard(mParams); 714 } 715 716 private void parseKeyboard(XmlPullParser parser) 717 throws XmlPullParserException, IOException { 718 if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_KEYBOARD, mParams.mId)); 719 int event; 720 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 721 if (event == XmlPullParser.START_TAG) { 722 final String tag = parser.getName(); 723 if (TAG_KEYBOARD.equals(tag)) { 724 parseKeyboardAttributes(parser); 725 startKeyboard(); 726 parseKeyboardContent(parser, false); 727 break; 728 } else { 729 throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEYBOARD); 730 } 731 } 732 } 733 } 734 735 private void parseKeyboardAttributes(XmlPullParser parser) { 736 final int displayWidth = mDisplayMetrics.widthPixels; 737 final TypedArray keyboardAttr = mContext.obtainStyledAttributes( 738 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle, 739 R.style.Keyboard); 740 final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 741 R.styleable.Keyboard_Key); 742 try { 743 final int displayHeight = mDisplayMetrics.heightPixels; 744 final int keyboardHeight = (int)keyboardAttr.getDimension( 745 R.styleable.Keyboard_keyboardHeight, displayHeight / 2); 746 final int maxKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, 747 R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2); 748 int minKeyboardHeight = (int)getDimensionOrFraction(keyboardAttr, 749 R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2); 750 if (minKeyboardHeight < 0) { 751 // Specified fraction was negative, so it should be calculated against display 752 // width. 753 minKeyboardHeight = -(int)getDimensionOrFraction(keyboardAttr, 754 R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2); 755 } 756 final Params params = mParams; 757 // Keyboard height will not exceed maxKeyboardHeight and will not be less than 758 // minKeyboardHeight. 759 params.mOccupiedHeight = Math.max( 760 Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight); 761 params.mOccupiedWidth = params.mId.mWidth; 762 params.mTopPadding = (int)getDimensionOrFraction(keyboardAttr, 763 R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0); 764 params.mBottomPadding = (int)getDimensionOrFraction(keyboardAttr, 765 R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0); 766 params.mHorizontalEdgesPadding = (int)getDimensionOrFraction(keyboardAttr, 767 R.styleable.Keyboard_keyboardHorizontalEdgesPadding, 768 mParams.mOccupiedWidth, 0); 769 770 params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2 771 - params.mHorizontalCenterPadding; 772 params.mDefaultKeyWidth = (int)getDimensionOrFraction(keyAttr, 773 R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth, 774 params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS); 775 params.mHorizontalGap = (int)getDimensionOrFraction(keyboardAttr, 776 R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0); 777 params.mVerticalGap = (int)getDimensionOrFraction(keyboardAttr, 778 R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0); 779 params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding 780 - params.mBottomPadding + params.mVerticalGap; 781 params.mDefaultRowHeight = (int)getDimensionOrFraction(keyboardAttr, 782 R.styleable.Keyboard_rowHeight, params.mBaseHeight, 783 params.mBaseHeight / DEFAULT_KEYBOARD_ROWS); 784 785 params.mIsRtlKeyboard = keyboardAttr.getBoolean( 786 R.styleable.Keyboard_isRtlKeyboard, false); 787 params.mMoreKeysTemplate = keyboardAttr.getResourceId( 788 R.styleable.Keyboard_moreKeysTemplate, 0); 789 params.mMaxMiniKeyboardColumn = keyAttr.getInt( 790 R.styleable.Keyboard_Key_maxMoreKeysColumn, 5); 791 792 params.mIconsSet.loadIcons(keyboardAttr); 793 } finally { 794 keyAttr.recycle(); 795 keyboardAttr.recycle(); 796 } 797 } 798 799 private void parseKeyboardContent(XmlPullParser parser, boolean skip) 800 throws XmlPullParserException, IOException { 801 int event; 802 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 803 if (event == XmlPullParser.START_TAG) { 804 final String tag = parser.getName(); 805 if (TAG_ROW.equals(tag)) { 806 Row row = parseRowAttributes(parser); 807 if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_ROW)); 808 if (!skip) 809 startRow(row); 810 parseRowContent(parser, row, skip); 811 } else if (TAG_INCLUDE.equals(tag)) { 812 parseIncludeKeyboardContent(parser, skip); 813 } else if (TAG_SWITCH.equals(tag)) { 814 parseSwitchKeyboardContent(parser, skip); 815 } else if (TAG_KEY_STYLE.equals(tag)) { 816 parseKeyStyle(parser, skip); 817 } else { 818 throw new XmlParseUtils.IllegalStartTag(parser, TAG_ROW); 819 } 820 } else if (event == XmlPullParser.END_TAG) { 821 final String tag = parser.getName(); 822 if (TAG_KEYBOARD.equals(tag)) { 823 endKeyboard(); 824 break; 825 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 826 || TAG_MERGE.equals(tag)) { 827 if (DEBUG) Log.d(TAG, String.format("</%s>", tag)); 828 break; 829 } else if (TAG_KEY_STYLE.equals(tag)) { 830 continue; 831 } else { 832 throw new XmlParseUtils.IllegalEndTag(parser, TAG_ROW); 833 } 834 } 835 } 836 } 837 838 private Row parseRowAttributes(XmlPullParser parser) throws XmlPullParserException { 839 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 840 R.styleable.Keyboard); 841 try { 842 if (a.hasValue(R.styleable.Keyboard_horizontalGap)) 843 throw new XmlParseUtils.IllegalAttribute(parser, "horizontalGap"); 844 if (a.hasValue(R.styleable.Keyboard_verticalGap)) 845 throw new XmlParseUtils.IllegalAttribute(parser, "verticalGap"); 846 return new Row(mResources, mParams, parser, mCurrentY); 847 } finally { 848 a.recycle(); 849 } 850 } 851 852 private void parseRowContent(XmlPullParser parser, Row row, boolean skip) 853 throws XmlPullParserException, IOException { 854 int event; 855 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 856 if (event == XmlPullParser.START_TAG) { 857 final String tag = parser.getName(); 858 if (TAG_KEY.equals(tag)) { 859 parseKey(parser, row, skip); 860 } else if (TAG_SPACER.equals(tag)) { 861 parseSpacer(parser, row, skip); 862 } else if (TAG_INCLUDE.equals(tag)) { 863 parseIncludeRowContent(parser, row, skip); 864 } else if (TAG_SWITCH.equals(tag)) { 865 parseSwitchRowContent(parser, row, skip); 866 } else if (TAG_KEY_STYLE.equals(tag)) { 867 parseKeyStyle(parser, skip); 868 } else { 869 throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); 870 } 871 } else if (event == XmlPullParser.END_TAG) { 872 final String tag = parser.getName(); 873 if (TAG_ROW.equals(tag)) { 874 if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_ROW)); 875 if (!skip) 876 endRow(row); 877 break; 878 } else if (TAG_CASE.equals(tag) || TAG_DEFAULT.equals(tag) 879 || TAG_MERGE.equals(tag)) { 880 if (DEBUG) Log.d(TAG, String.format("</%s>", tag)); 881 break; 882 } else if (TAG_KEY_STYLE.equals(tag)) { 883 continue; 884 } else { 885 throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); 886 } 887 } 888 } 889 } 890 891 private void parseKey(XmlPullParser parser, Row row, boolean skip) 892 throws XmlPullParserException, IOException { 893 if (skip) { 894 XmlParseUtils.checkEndTag(TAG_KEY, parser); 895 } else { 896 final Key key = new Key(mResources, mParams, row, parser, mKeyStyles); 897 if (DEBUG) Log.d(TAG, String.format("<%s%s keyLabel=%s code=%d moreKeys=%s />", 898 TAG_KEY, (key.isEnabled() ? "" : " disabled"), key.mLabel, key.mCode, 899 Arrays.toString(key.mMoreKeys))); 900 XmlParseUtils.checkEndTag(TAG_KEY, parser); 901 endKey(key); 902 } 903 } 904 905 private void parseSpacer(XmlPullParser parser, Row row, boolean skip) 906 throws XmlPullParserException, IOException { 907 if (skip) { 908 XmlParseUtils.checkEndTag(TAG_SPACER, parser); 909 } else { 910 final Key.Spacer spacer = new Key.Spacer( 911 mResources, mParams, row, parser, mKeyStyles); 912 if (DEBUG) Log.d(TAG, String.format("<%s />", TAG_SPACER)); 913 XmlParseUtils.checkEndTag(TAG_SPACER, parser); 914 endKey(spacer); 915 } 916 } 917 918 private void parseIncludeKeyboardContent(XmlPullParser parser, boolean skip) 919 throws XmlPullParserException, IOException { 920 parseIncludeInternal(parser, null, skip); 921 } 922 923 private void parseIncludeRowContent(XmlPullParser parser, Row row, boolean skip) 924 throws XmlPullParserException, IOException { 925 parseIncludeInternal(parser, row, skip); 926 } 927 928 private void parseIncludeInternal(XmlPullParser parser, Row row, boolean skip) 929 throws XmlPullParserException, IOException { 930 if (skip) { 931 XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); 932 } else { 933 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 934 R.styleable.Keyboard_Include); 935 int keyboardLayout = 0; 936 try { 937 XmlParseUtils.checkAttributeExists(a, 938 R.styleable.Keyboard_Include_keyboardLayout, "keyboardLayout", 939 TAG_INCLUDE, parser); 940 keyboardLayout = a.getResourceId( 941 R.styleable.Keyboard_Include_keyboardLayout, 0); 942 } finally { 943 a.recycle(); 944 } 945 946 XmlParseUtils.checkEndTag(TAG_INCLUDE, parser); 947 if (DEBUG) Log.d(TAG, String.format("<%s keyboardLayout=%s />", 948 TAG_INCLUDE, mResources.getResourceEntryName(keyboardLayout))); 949 final XmlResourceParser parserForInclude = mResources.getXml(keyboardLayout); 950 try { 951 parseMerge(parserForInclude, row, skip); 952 } finally { 953 parserForInclude.close(); 954 } 955 } 956 } 957 958 private void parseMerge(XmlPullParser parser, Row row, boolean skip) 959 throws XmlPullParserException, IOException { 960 int event; 961 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 962 if (event == XmlPullParser.START_TAG) { 963 final String tag = parser.getName(); 964 if (TAG_MERGE.equals(tag)) { 965 if (row == null) { 966 parseKeyboardContent(parser, skip); 967 } else { 968 parseRowContent(parser, row, skip); 969 } 970 break; 971 } else { 972 throw new XmlParseUtils.ParseException( 973 "Included keyboard layout must have <merge> root element", parser); 974 } 975 } 976 } 977 } 978 979 private void parseSwitchKeyboardContent(XmlPullParser parser, boolean skip) 980 throws XmlPullParserException, IOException { 981 parseSwitchInternal(parser, null, skip); 982 } 983 984 private void parseSwitchRowContent(XmlPullParser parser, Row row, boolean skip) 985 throws XmlPullParserException, IOException { 986 parseSwitchInternal(parser, row, skip); 987 } 988 989 private void parseSwitchInternal(XmlPullParser parser, Row row, boolean skip) 990 throws XmlPullParserException, IOException { 991 if (DEBUG) Log.d(TAG, String.format("<%s> %s", TAG_SWITCH, mParams.mId)); 992 boolean selected = false; 993 int event; 994 while ((event = parser.next()) != XmlPullParser.END_DOCUMENT) { 995 if (event == XmlPullParser.START_TAG) { 996 final String tag = parser.getName(); 997 if (TAG_CASE.equals(tag)) { 998 selected |= parseCase(parser, row, selected ? true : skip); 999 } else if (TAG_DEFAULT.equals(tag)) { 1000 selected |= parseDefault(parser, row, selected ? true : skip); 1001 } else { 1002 throw new XmlParseUtils.IllegalStartTag(parser, TAG_KEY); 1003 } 1004 } else if (event == XmlPullParser.END_TAG) { 1005 final String tag = parser.getName(); 1006 if (TAG_SWITCH.equals(tag)) { 1007 if (DEBUG) Log.d(TAG, String.format("</%s>", TAG_SWITCH)); 1008 break; 1009 } else { 1010 throw new XmlParseUtils.IllegalEndTag(parser, TAG_KEY); 1011 } 1012 } 1013 } 1014 } 1015 1016 private boolean parseCase(XmlPullParser parser, Row row, boolean skip) 1017 throws XmlPullParserException, IOException { 1018 final boolean selected = parseCaseCondition(parser); 1019 if (row == null) { 1020 // Processing Rows. 1021 parseKeyboardContent(parser, selected ? skip : true); 1022 } else { 1023 // Processing Keys. 1024 parseRowContent(parser, row, selected ? skip : true); 1025 } 1026 return selected; 1027 } 1028 1029 private boolean parseCaseCondition(XmlPullParser parser) { 1030 final KeyboardId id = mParams.mId; 1031 if (id == null) 1032 return true; 1033 1034 final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), 1035 R.styleable.Keyboard_Case); 1036 try { 1037 final boolean keyboardSetElementMatched = matchTypedValue(a, 1038 R.styleable.Keyboard_Case_keyboardSetElement, id.mElementId, 1039 KeyboardId.elementIdToName(id.mElementId)); 1040 final boolean modeMatched = matchTypedValue(a, 1041 R.styleable.Keyboard_Case_mode, id.mMode, KeyboardId.modeName(id.mMode)); 1042 final boolean navigateActionMatched = matchBoolean(a, 1043 R.styleable.Keyboard_Case_navigateAction, id.navigateAction()); 1044 final boolean passwordInputMatched = matchBoolean(a, 1045 R.styleable.Keyboard_Case_passwordInput, id.passwordInput()); 1046 final boolean hasSettingsKeyMatched = matchBoolean(a, 1047 R.styleable.Keyboard_Case_hasSettingsKey, id.hasSettingsKey()); 1048 final boolean f2KeyModeMatched = matchInteger(a, 1049 R.styleable.Keyboard_Case_f2KeyMode, id.f2KeyMode()); 1050 final boolean clobberSettingsKeyMatched = matchBoolean(a, 1051 R.styleable.Keyboard_Case_clobberSettingsKey, id.mClobberSettingsKey); 1052 final boolean shortcutKeyEnabledMatched = matchBoolean(a, 1053 R.styleable.Keyboard_Case_shortcutKeyEnabled, id.mShortcutKeyEnabled); 1054 final boolean hasShortcutKeyMatched = matchBoolean(a, 1055 R.styleable.Keyboard_Case_hasShortcutKey, id.mHasShortcutKey); 1056 // As noted at {@link KeyboardId} class, we are interested only in enum value 1057 // masked by {@link android.view.inputmethod.EditorInfo#IME_MASK_ACTION} and 1058 // {@link android.view.inputmethod.EditorInfo#IME_FLAG_NO_ENTER_ACTION}. So matching 1059 // this attribute with id.mImeOptions as integer value is enough for our purpose. 1060 final boolean imeActionMatched = matchInteger(a, 1061 R.styleable.Keyboard_Case_imeAction, id.imeAction()); 1062 final boolean localeCodeMatched = matchString(a, 1063 R.styleable.Keyboard_Case_localeCode, id.mLocale.toString()); 1064 final boolean languageCodeMatched = matchString(a, 1065 R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage()); 1066 final boolean countryCodeMatched = matchString(a, 1067 R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry()); 1068 final boolean selected = keyboardSetElementMatched && modeMatched 1069 && navigateActionMatched && passwordInputMatched && hasSettingsKeyMatched 1070 && f2KeyModeMatched && clobberSettingsKeyMatched 1071 && shortcutKeyEnabledMatched && hasShortcutKeyMatched && imeActionMatched 1072 && localeCodeMatched && languageCodeMatched && countryCodeMatched; 1073 1074 if (DEBUG) Log.d(TAG, String.format("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s> %s", TAG_CASE, 1075 textAttr(a.getString(R.styleable.Keyboard_Case_keyboardSetElement), 1076 "keyboardSetElement"), 1077 textAttr(a.getString(R.styleable.Keyboard_Case_mode), "mode"), 1078 booleanAttr(a, R.styleable.Keyboard_Case_navigateAction, "navigateAction"), 1079 booleanAttr(a, R.styleable.Keyboard_Case_passwordInput, "passwordInput"), 1080 booleanAttr(a, R.styleable.Keyboard_Case_hasSettingsKey, "hasSettingsKey"), 1081 textAttr(KeyboardId.f2KeyModeName( 1082 a.getInt(R.styleable.Keyboard_Case_f2KeyMode, -1)), "f2KeyMode"), 1083 booleanAttr(a, R.styleable.Keyboard_Case_clobberSettingsKey, 1084 "clobberSettingsKey"), 1085 booleanAttr(a, R.styleable.Keyboard_Case_shortcutKeyEnabled, 1086 "shortcutKeyEnabled"), 1087 booleanAttr(a, R.styleable.Keyboard_Case_hasShortcutKey, "hasShortcutKey"), 1088 textAttr(EditorInfoCompatUtils.imeOptionsName( 1089 a.getInt(R.styleable.Keyboard_Case_imeAction, -1)), "imeAction"), 1090 textAttr(a.getString(R.styleable.Keyboard_Case_localeCode), "localeCode"), 1091 textAttr(a.getString(R.styleable.Keyboard_Case_languageCode), 1092 "languageCode"), 1093 textAttr(a.getString(R.styleable.Keyboard_Case_countryCode), "countryCode"), 1094 Boolean.toString(selected))); 1095 1096 return selected; 1097 } finally { 1098 a.recycle(); 1099 } 1100 } 1101 1102 private static boolean matchInteger(TypedArray a, int index, int value) { 1103 // If <case> does not have "index" attribute, that means this <case> is wild-card for 1104 // the attribute. 1105 return !a.hasValue(index) || a.getInt(index, 0) == value; 1106 } 1107 1108 private static boolean matchBoolean(TypedArray a, int index, boolean value) { 1109 // If <case> does not have "index" attribute, that means this <case> is wild-card for 1110 // the attribute. 1111 return !a.hasValue(index) || a.getBoolean(index, false) == value; 1112 } 1113 1114 private static boolean matchString(TypedArray a, int index, String value) { 1115 // If <case> does not have "index" attribute, that means this <case> is wild-card for 1116 // the attribute. 1117 return !a.hasValue(index) 1118 || stringArrayContains(a.getString(index).split("\\|"), value); 1119 } 1120 1121 private static boolean matchTypedValue(TypedArray a, int index, int intValue, 1122 String strValue) { 1123 // If <case> does not have "index" attribute, that means this <case> is wild-card for 1124 // the attribute. 1125 final TypedValue v = a.peekValue(index); 1126 if (v == null) 1127 return true; 1128 1129 if (isIntegerValue(v)) { 1130 return intValue == a.getInt(index, 0); 1131 } else if (isStringValue(v)) { 1132 return stringArrayContains(a.getString(index).split("\\|"), strValue); 1133 } 1134 return false; 1135 } 1136 1137 private static boolean stringArrayContains(String[] array, String value) { 1138 for (final String elem : array) { 1139 if (elem.equals(value)) 1140 return true; 1141 } 1142 return false; 1143 } 1144 1145 private boolean parseDefault(XmlPullParser parser, Row row, boolean skip) 1146 throws XmlPullParserException, IOException { 1147 if (DEBUG) Log.d(TAG, String.format("<%s>", TAG_DEFAULT)); 1148 if (row == null) { 1149 parseKeyboardContent(parser, skip); 1150 } else { 1151 parseRowContent(parser, row, skip); 1152 } 1153 return true; 1154 } 1155 1156 private void parseKeyStyle(XmlPullParser parser, boolean skip) 1157 throws XmlPullParserException { 1158 TypedArray keyStyleAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser), 1159 R.styleable.Keyboard_KeyStyle); 1160 TypedArray keyAttrs = mResources.obtainAttributes(Xml.asAttributeSet(parser), 1161 R.styleable.Keyboard_Key); 1162 try { 1163 if (!keyStyleAttr.hasValue(R.styleable.Keyboard_KeyStyle_styleName)) 1164 throw new XmlParseUtils.ParseException("<" + TAG_KEY_STYLE 1165 + "/> needs styleName attribute", parser); 1166 if (!skip) 1167 mKeyStyles.parseKeyStyleAttributes(keyStyleAttr, keyAttrs, parser); 1168 } finally { 1169 keyStyleAttr.recycle(); 1170 keyAttrs.recycle(); 1171 } 1172 } 1173 1174 private void startKeyboard() { 1175 mCurrentY += mParams.mTopPadding; 1176 mTopEdge = true; 1177 } 1178 1179 private void startRow(Row row) { 1180 addEdgeSpace(mParams.mHorizontalEdgesPadding, row); 1181 mCurrentRow = row; 1182 mLeftEdge = true; 1183 mRightEdgeKey = null; 1184 } 1185 1186 private void endRow(Row row) { 1187 if (mCurrentRow == null) 1188 throw new InflateException("orphant end row tag"); 1189 if (mRightEdgeKey != null) { 1190 mRightEdgeKey.markAsRightEdge(mParams); 1191 mRightEdgeKey = null; 1192 } 1193 addEdgeSpace(mParams.mHorizontalEdgesPadding, row); 1194 mCurrentY += row.mRowHeight; 1195 mCurrentRow = null; 1196 mTopEdge = false; 1197 } 1198 1199 private void endKey(Key key) { 1200 mParams.onAddKey(key); 1201 if (mLeftEdge) { 1202 key.markAsLeftEdge(mParams); 1203 mLeftEdge = false; 1204 } 1205 if (mTopEdge) { 1206 key.markAsTopEdge(mParams); 1207 } 1208 mRightEdgeKey = key; 1209 } 1210 1211 private void endKeyboard() { 1212 // nothing to do here. 1213 } 1214 1215 private void addEdgeSpace(float width, Row row) { 1216 row.advanceXPos(width); 1217 mLeftEdge = false; 1218 mRightEdgeKey = null; 1219 } 1220 1221 public static float getDimensionOrFraction(TypedArray a, int index, int base, 1222 float defValue) { 1223 final TypedValue value = a.peekValue(index); 1224 if (value == null) 1225 return defValue; 1226 if (isFractionValue(value)) { 1227 return a.getFraction(index, base, base, defValue); 1228 } else if (isDimensionValue(value)) { 1229 return a.getDimension(index, defValue); 1230 } 1231 return defValue; 1232 } 1233 1234 public static int getEnumValue(TypedArray a, int index, int defValue) { 1235 final TypedValue value = a.peekValue(index); 1236 if (value == null) 1237 return defValue; 1238 if (isIntegerValue(value)) { 1239 return a.getInt(index, defValue); 1240 } 1241 return defValue; 1242 } 1243 1244 private static boolean isFractionValue(TypedValue v) { 1245 return v.type == TypedValue.TYPE_FRACTION; 1246 } 1247 1248 private static boolean isDimensionValue(TypedValue v) { 1249 return v.type == TypedValue.TYPE_DIMENSION; 1250 } 1251 1252 private static boolean isIntegerValue(TypedValue v) { 1253 return v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT; 1254 } 1255 1256 private static boolean isStringValue(TypedValue v) { 1257 return v.type == TypedValue.TYPE_STRING; 1258 } 1259 1260 private static String textAttr(String value, String name) { 1261 return value != null ? String.format(" %s=%s", name, value) : ""; 1262 } 1263 1264 private static String booleanAttr(TypedArray a, int index, String name) { 1265 return a.hasValue(index) 1266 ? String.format(" %s=%s", name, a.getBoolean(index, false)) : ""; 1267 } 1268 } 1269} 1270