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