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