1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.keyboard; 18 19 import static com.android.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED; 20 import static com.android.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT; 21 import static com.android.inputmethod.latin.common.Constants.CODE_SHIFT; 22 import static com.android.inputmethod.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL; 23 import static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED; 24 25 import android.content.res.TypedArray; 26 import android.graphics.Rect; 27 import android.graphics.Typeface; 28 import android.graphics.drawable.Drawable; 29 import android.text.TextUtils; 30 31 import com.android.inputmethod.keyboard.internal.KeyDrawParams; 32 import com.android.inputmethod.keyboard.internal.KeySpecParser; 33 import com.android.inputmethod.keyboard.internal.KeyStyle; 34 import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; 35 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 36 import com.android.inputmethod.keyboard.internal.KeyboardParams; 37 import com.android.inputmethod.keyboard.internal.KeyboardRow; 38 import com.android.inputmethod.keyboard.internal.MoreKeySpec; 39 import com.android.inputmethod.latin.R; 40 import com.android.inputmethod.latin.common.Constants; 41 import com.android.inputmethod.latin.common.StringUtils; 42 43 import java.util.Arrays; 44 import java.util.Locale; 45 46 import javax.annotation.Nonnull; 47 import javax.annotation.Nullable; 48 49 /** 50 * Class for describing the position and characteristics of a single key in the keyboard. 51 */ 52 public class Key implements Comparable<Key> { 53 /** 54 * The key code (unicode or custom code) that this key generates. 55 */ 56 private final int mCode; 57 58 /** Label to display */ 59 private final String mLabel; 60 /** Hint label to display on the key in conjunction with the label */ 61 private final String mHintLabel; 62 /** Flags of the label */ 63 private final int mLabelFlags; 64 private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02; 65 private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04; 66 private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08; 67 // Font typeface specification. 68 private static final int LABEL_FLAGS_FONT_MASK = 0x30; 69 private static final int LABEL_FLAGS_FONT_NORMAL = 0x10; 70 private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20; 71 private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30; 72 // Start of key text ratio enum values 73 private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0; 74 private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40; 75 private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80; 76 private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0; 77 private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140; 78 // End of key text ratio mask enum values 79 private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200; 80 private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400; 81 private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800; 82 // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on 83 // and autoYScale bit is off, the key label may be shrunk only for X-direction. 84 // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled. 85 private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000; 86 private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000; 87 private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE 88 | LABEL_FLAGS_AUTO_Y_SCALE; 89 private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000; 90 private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000; 91 private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000; 92 private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000; 93 private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000; 94 private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000; 95 private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000; 96 97 /** Icon to display instead of a label. Icon takes precedence over a label */ 98 private final int mIconId; 99 100 /** Width of the key, excluding the gap */ 101 private final int mWidth; 102 /** Height of the key, excluding the gap */ 103 private final int mHeight; 104 /** 105 * The combined width in pixels of the horizontal gaps belonging to this key, both to the left 106 * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key. 107 */ 108 private final int mHorizontalGap; 109 /** 110 * The combined height in pixels of the vertical gaps belonging to this key, both above and 111 * below. I.e., mHeight + mVerticalGap = total height belonging to the key. 112 */ 113 private final int mVerticalGap; 114 /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ 115 private final int mX; 116 /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ 117 private final int mY; 118 /** Hit bounding box of the key */ 119 @Nonnull 120 private final Rect mHitBox = new Rect(); 121 122 /** More keys. It is guaranteed that this is null or an array of one or more elements */ 123 @Nullable 124 private final MoreKeySpec[] mMoreKeys; 125 /** More keys column number and flags */ 126 private final int mMoreKeysColumnAndFlags; 127 private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff; 128 // If this flag is specified, more keys keyboard should have the specified number of columns. 129 // Otherwise more keys keyboard should have less than or equal to the specified maximum number 130 // of columns. 131 private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100; 132 // If this flag is specified, the order of more keys is determined by the order in the more 133 // keys' specification. Otherwise the order of more keys is automatically determined. 134 private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200; 135 private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0; 136 private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER = 137 MORE_KEYS_FLAGS_FIXED_COLUMN; 138 private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER = 139 (MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER); 140 private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000; 141 private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000; 142 private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000; 143 // TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively. 144 private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!"; 145 private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!"; 146 private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!"; 147 private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!"; 148 private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!"; 149 150 /** Background type that represents different key background visual than normal one. */ 151 private final int mBackgroundType; 152 public static final int BACKGROUND_TYPE_EMPTY = 0; 153 public static final int BACKGROUND_TYPE_NORMAL = 1; 154 public static final int BACKGROUND_TYPE_FUNCTIONAL = 2; 155 public static final int BACKGROUND_TYPE_STICKY_OFF = 3; 156 public static final int BACKGROUND_TYPE_STICKY_ON = 4; 157 public static final int BACKGROUND_TYPE_ACTION = 5; 158 public static final int BACKGROUND_TYPE_SPACEBAR = 6; 159 160 private final int mActionFlags; 161 private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01; 162 private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02; 163 private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04; 164 private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08; 165 166 @Nullable 167 private final KeyVisualAttributes mKeyVisualAttributes; 168 @Nullable 169 private final OptionalAttributes mOptionalAttributes; 170 171 private static final class OptionalAttributes { 172 /** Text to output when pressed. This can be multiple characters, like ".com" */ 173 public final String mOutputText; 174 public final int mAltCode; 175 /** Icon for disabled state */ 176 public final int mDisabledIconId; 177 /** The visual insets */ 178 public final int mVisualInsetsLeft; 179 public final int mVisualInsetsRight; 180 181 private OptionalAttributes(final String outputText, final int altCode, 182 final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { 183 mOutputText = outputText; 184 mAltCode = altCode; 185 mDisabledIconId = disabledIconId; 186 mVisualInsetsLeft = visualInsetsLeft; 187 mVisualInsetsRight = visualInsetsRight; 188 } 189 190 @Nullable 191 public static OptionalAttributes newInstance(final String outputText, final int altCode, 192 final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { 193 if (outputText == null && altCode == CODE_UNSPECIFIED 194 && disabledIconId == ICON_UNDEFINED && visualInsetsLeft == 0 195 && visualInsetsRight == 0) { 196 return null; 197 } 198 return new OptionalAttributes(outputText, altCode, disabledIconId, visualInsetsLeft, 199 visualInsetsRight); 200 } 201 } 202 203 private final int mHashCode; 204 205 /** The current pressed state of this key */ 206 private boolean mPressed; 207 /** Key is enabled and responds on press */ 208 private boolean mEnabled = true; 209 210 /** 211 * Constructor for a key on <code>MoreKeyKeyboard</code>, on <code>MoreSuggestions</code>, 212 * and in a <GridRows/>. 213 */ 214 public Key(@Nullable final String label, final int iconId, final int code, 215 @Nullable final String outputText, @Nullable final String hintLabel, 216 final int labelFlags, final int backgroundType, final int x, final int y, 217 final int width, final int height, final int horizontalGap, final int verticalGap) { 218 mWidth = width - horizontalGap; 219 mHeight = height - verticalGap; 220 mHorizontalGap = horizontalGap; 221 mVerticalGap = verticalGap; 222 mHintLabel = hintLabel; 223 mLabelFlags = labelFlags; 224 mBackgroundType = backgroundType; 225 // TODO: Pass keyActionFlags as an argument. 226 mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW; 227 mMoreKeys = null; 228 mMoreKeysColumnAndFlags = 0; 229 mLabel = label; 230 mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED, 231 ICON_UNDEFINED, 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */); 232 mCode = code; 233 mEnabled = (code != CODE_UNSPECIFIED); 234 mIconId = iconId; 235 // Horizontal gap is divided equally to both sides of the key. 236 mX = x + mHorizontalGap / 2; 237 mY = y; 238 mHitBox.set(x, y, x + width + 1, y + height); 239 mKeyVisualAttributes = null; 240 241 mHashCode = computeHashCode(this); 242 } 243 244 /** 245 * Create a key with the given top-left coordinate and extract its attributes from a key 246 * specification string, Key attribute array, key style, and etc. 247 * 248 * @param keySpec the key specification. 249 * @param keyAttr the Key XML attributes array. 250 * @param style the {@link KeyStyle} of this key. 251 * @param params the keyboard building parameters. 252 * @param row the row that this key belongs to. row's x-coordinate will be the right edge of 253 * this key. 254 */ 255 public Key(@Nullable final String keySpec, @Nonnull final TypedArray keyAttr, 256 @Nonnull final KeyStyle style, @Nonnull final KeyboardParams params, 257 @Nonnull final KeyboardRow row) { 258 mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap; 259 mVerticalGap = params.mVerticalGap; 260 261 final float horizontalGapFloat = mHorizontalGap; 262 final int rowHeight = row.getRowHeight(); 263 mHeight = rowHeight - mVerticalGap; 264 265 final float keyXPos = row.getKeyX(keyAttr); 266 final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); 267 final int keyYPos = row.getKeyY(); 268 269 // Horizontal gap is divided equally to both sides of the key. 270 mX = Math.round(keyXPos + horizontalGapFloat / 2); 271 mY = keyYPos; 272 mWidth = Math.round(keyWidth - horizontalGapFloat); 273 mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, 274 keyYPos + rowHeight); 275 // Update row to have current x coordinate. 276 row.setXPos(keyXPos + keyWidth); 277 278 mBackgroundType = style.getInt(keyAttr, 279 R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType()); 280 281 final int baseWidth = params.mBaseWidth; 282 final int visualInsetsLeft = Math.round(keyAttr.getFraction( 283 R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0)); 284 final int visualInsetsRight = Math.round(keyAttr.getFraction( 285 R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0)); 286 287 mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) 288 | row.getDefaultKeyLabelFlags(); 289 final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId); 290 final Locale localeForUpcasing = params.mId.getLocale(); 291 int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); 292 String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); 293 294 // Get maximum column order number and set a relevant mode value. 295 int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER 296 | style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, 297 params.mMaxMoreKeysKeyboardColumn); 298 int value; 299 if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { 300 // Override with fixed column order number and set a relevant mode value. 301 moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER 302 | (value & MORE_KEYS_COLUMN_NUMBER_MASK); 303 } 304 if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { 305 // Override with fixed column order number and set a relevant mode value. 306 moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER 307 | (value & MORE_KEYS_COLUMN_NUMBER_MASK); 308 } 309 if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { 310 moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS; 311 } 312 if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { 313 moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; 314 } 315 if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { 316 moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; 317 } 318 mMoreKeysColumnAndFlags = moreKeysColumnAndFlags; 319 320 final String[] additionalMoreKeys; 321 if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) { 322 additionalMoreKeys = null; 323 } else { 324 additionalMoreKeys = style.getStringArray(keyAttr, 325 R.styleable.Keyboard_Key_additionalMoreKeys); 326 } 327 moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); 328 if (moreKeys != null) { 329 actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; 330 mMoreKeys = new MoreKeySpec[moreKeys.length]; 331 for (int i = 0; i < moreKeys.length; i++) { 332 mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing); 333 } 334 } else { 335 mMoreKeys = null; 336 } 337 mActionFlags = actionFlags; 338 339 mIconId = KeySpecParser.getIconId(keySpec); 340 final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, 341 R.styleable.Keyboard_Key_keyIconDisabled)); 342 343 final int code = KeySpecParser.getCode(keySpec); 344 if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) { 345 mLabel = params.mId.mCustomActionLabel; 346 } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { 347 // This is a workaround to have a key that has a supplementary code point in its label. 348 // Because we can put a string in resource neither as a XML entity of a supplementary 349 // code point nor as a surrogate pair. 350 mLabel = new StringBuilder().appendCodePoint(code).toString(); 351 } else { 352 final String label = KeySpecParser.getLabel(keySpec); 353 mLabel = needsToUpcase 354 ? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing) 355 : label; 356 } 357 if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { 358 mHintLabel = null; 359 } else { 360 final String hintLabel = style.getString( 361 keyAttr, R.styleable.Keyboard_Key_keyHintLabel); 362 mHintLabel = needsToUpcase 363 ? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing) 364 : hintLabel; 365 } 366 String outputText = KeySpecParser.getOutputText(keySpec); 367 if (needsToUpcase) { 368 outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing); 369 } 370 // Choose the first letter of the label as primary code if not specified. 371 if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) 372 && !TextUtils.isEmpty(mLabel)) { 373 if (StringUtils.codePointCount(mLabel) == 1) { 374 // Use the first letter of the hint label if shiftedLetterActivated flag is 375 // specified. 376 if (hasShiftedLetterHint() && isShiftedLetterActivated()) { 377 mCode = mHintLabel.codePointAt(0); 378 } else { 379 mCode = mLabel.codePointAt(0); 380 } 381 } else { 382 // In some locale and case, the character might be represented by multiple code 383 // points, such as upper case Eszett of German alphabet. 384 outputText = mLabel; 385 mCode = CODE_OUTPUT_TEXT; 386 } 387 } else if (code == CODE_UNSPECIFIED && outputText != null) { 388 if (StringUtils.codePointCount(outputText) == 1) { 389 mCode = outputText.codePointAt(0); 390 outputText = null; 391 } else { 392 mCode = CODE_OUTPUT_TEXT; 393 } 394 } else { 395 mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing) 396 : code; 397 } 398 final int altCodeInAttr = KeySpecParser.parseCode( 399 style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); 400 final int altCode = needsToUpcase 401 ? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing) 402 : altCodeInAttr; 403 mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, 404 disabledIconId, visualInsetsLeft, visualInsetsRight); 405 mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); 406 mHashCode = computeHashCode(this); 407 } 408 409 /** 410 * Copy constructor for DynamicGridKeyboard.GridKey. 411 * 412 * @param key the original key. 413 */ 414 protected Key(@Nonnull final Key key) { 415 this(key, key.mMoreKeys); 416 } 417 418 private Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys) { 419 // Final attributes. 420 mCode = key.mCode; 421 mLabel = key.mLabel; 422 mHintLabel = key.mHintLabel; 423 mLabelFlags = key.mLabelFlags; 424 mIconId = key.mIconId; 425 mWidth = key.mWidth; 426 mHeight = key.mHeight; 427 mHorizontalGap = key.mHorizontalGap; 428 mVerticalGap = key.mVerticalGap; 429 mX = key.mX; 430 mY = key.mY; 431 mHitBox.set(key.mHitBox); 432 mMoreKeys = moreKeys; 433 mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags; 434 mBackgroundType = key.mBackgroundType; 435 mActionFlags = key.mActionFlags; 436 mKeyVisualAttributes = key.mKeyVisualAttributes; 437 mOptionalAttributes = key.mOptionalAttributes; 438 mHashCode = key.mHashCode; 439 // Key state. 440 mPressed = key.mPressed; 441 mEnabled = key.mEnabled; 442 } 443 444 @Nonnull 445 public static Key removeRedundantMoreKeys(@Nonnull final Key key, 446 @Nonnull final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) { 447 final MoreKeySpec[] moreKeys = key.getMoreKeys(); 448 final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys( 449 moreKeys, lettersOnBaseLayout); 450 return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys); 451 } 452 453 private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) { 454 if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false; 455 switch (keyboardElementId) { 456 case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: 457 case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: 458 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: 459 case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: 460 return true; 461 default: 462 return false; 463 } 464 } 465 466 private static int computeHashCode(final Key key) { 467 return Arrays.hashCode(new Object[] { 468 key.mX, 469 key.mY, 470 key.mWidth, 471 key.mHeight, 472 key.mCode, 473 key.mLabel, 474 key.mHintLabel, 475 key.mIconId, 476 key.mBackgroundType, 477 Arrays.hashCode(key.mMoreKeys), 478 key.getOutputText(), 479 key.mActionFlags, 480 key.mLabelFlags, 481 // Key can be distinguishable without the following members. 482 // key.mOptionalAttributes.mAltCode, 483 // key.mOptionalAttributes.mDisabledIconId, 484 // key.mOptionalAttributes.mPreviewIconId, 485 // key.mHorizontalGap, 486 // key.mVerticalGap, 487 // key.mOptionalAttributes.mVisualInsetLeft, 488 // key.mOptionalAttributes.mVisualInsetRight, 489 // key.mMaxMoreKeysColumn, 490 }); 491 } 492 493 private boolean equalsInternal(final Key o) { 494 if (this == o) return true; 495 return o.mX == mX 496 && o.mY == mY 497 && o.mWidth == mWidth 498 && o.mHeight == mHeight 499 && o.mCode == mCode 500 && TextUtils.equals(o.mLabel, mLabel) 501 && TextUtils.equals(o.mHintLabel, mHintLabel) 502 && o.mIconId == mIconId 503 && o.mBackgroundType == mBackgroundType 504 && Arrays.equals(o.mMoreKeys, mMoreKeys) 505 && TextUtils.equals(o.getOutputText(), getOutputText()) 506 && o.mActionFlags == mActionFlags 507 && o.mLabelFlags == mLabelFlags; 508 } 509 510 @Override 511 public int compareTo(Key o) { 512 if (equalsInternal(o)) return 0; 513 if (mHashCode > o.mHashCode) return 1; 514 return -1; 515 } 516 517 @Override 518 public int hashCode() { 519 return mHashCode; 520 } 521 522 @Override 523 public boolean equals(final Object o) { 524 return o instanceof Key && equalsInternal((Key)o); 525 } 526 527 @Override 528 public String toString() { 529 return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight(); 530 } 531 532 public String toShortString() { 533 final int code = getCode(); 534 if (code == Constants.CODE_OUTPUT_TEXT) { 535 return getOutputText(); 536 } 537 return Constants.printableCode(code); 538 } 539 540 public String toLongString() { 541 final int iconId = getIconId(); 542 final String topVisual = (iconId == KeyboardIconsSet.ICON_UNDEFINED) 543 ? KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(iconId) : getLabel(); 544 final String hintLabel = getHintLabel(); 545 final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel; 546 return toString() + " " + visual + "/" + backgroundName(mBackgroundType); 547 } 548 549 private static String backgroundName(final int backgroundType) { 550 switch (backgroundType) { 551 case BACKGROUND_TYPE_EMPTY: return "empty"; 552 case BACKGROUND_TYPE_NORMAL: return "normal"; 553 case BACKGROUND_TYPE_FUNCTIONAL: return "functional"; 554 case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff"; 555 case BACKGROUND_TYPE_STICKY_ON: return "stickyOn"; 556 case BACKGROUND_TYPE_ACTION: return "action"; 557 case BACKGROUND_TYPE_SPACEBAR: return "spacebar"; 558 default: return null; 559 } 560 } 561 562 public int getCode() { 563 return mCode; 564 } 565 566 @Nullable 567 public String getLabel() { 568 return mLabel; 569 } 570 571 @Nullable 572 public String getHintLabel() { 573 return mHintLabel; 574 } 575 576 @Nullable 577 public MoreKeySpec[] getMoreKeys() { 578 return mMoreKeys; 579 } 580 581 public void markAsLeftEdge(final KeyboardParams params) { 582 mHitBox.left = params.mLeftPadding; 583 } 584 585 public void markAsRightEdge(final KeyboardParams params) { 586 mHitBox.right = params.mOccupiedWidth - params.mRightPadding; 587 } 588 589 public void markAsTopEdge(final KeyboardParams params) { 590 mHitBox.top = params.mTopPadding; 591 } 592 593 public void markAsBottomEdge(final KeyboardParams params) { 594 mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding; 595 } 596 597 public final boolean isSpacer() { 598 return this instanceof Spacer; 599 } 600 601 public final boolean isActionKey() { 602 return mBackgroundType == BACKGROUND_TYPE_ACTION; 603 } 604 605 public final boolean isShift() { 606 return mCode == CODE_SHIFT; 607 } 608 609 public final boolean isModifier() { 610 return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL; 611 } 612 613 public final boolean isRepeatable() { 614 return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0; 615 } 616 617 public final boolean noKeyPreview() { 618 return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0; 619 } 620 621 public final boolean altCodeWhileTyping() { 622 return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0; 623 } 624 625 public final boolean isLongPressEnabled() { 626 // We need not start long press timer on the key which has activated shifted letter. 627 return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0 628 && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0; 629 } 630 631 public KeyVisualAttributes getVisualAttributes() { 632 return mKeyVisualAttributes; 633 } 634 635 @Nonnull 636 public final Typeface selectTypeface(final KeyDrawParams params) { 637 switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) { 638 case LABEL_FLAGS_FONT_NORMAL: 639 return Typeface.DEFAULT; 640 case LABEL_FLAGS_FONT_MONO_SPACE: 641 return Typeface.MONOSPACE; 642 case LABEL_FLAGS_FONT_DEFAULT: 643 default: 644 // The type-face is specified by keyTypeface attribute. 645 return params.mTypeface; 646 } 647 } 648 649 public final int selectTextSize(final KeyDrawParams params) { 650 switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) { 651 case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO: 652 return params.mLetterSize; 653 case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO: 654 return params.mLargeLetterSize; 655 case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO: 656 return params.mLabelSize; 657 case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO: 658 return params.mHintLabelSize; 659 default: // No follow key ratio flag specified. 660 return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize; 661 } 662 } 663 664 public final int selectTextColor(final KeyDrawParams params) { 665 if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { 666 return params.mFunctionalTextColor; 667 } 668 return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; 669 } 670 671 public final int selectHintTextSize(final KeyDrawParams params) { 672 if (hasHintLabel()) { 673 return params.mHintLabelSize; 674 } 675 if (hasShiftedLetterHint()) { 676 return params.mShiftedLetterHintSize; 677 } 678 return params.mHintLetterSize; 679 } 680 681 public final int selectHintTextColor(final KeyDrawParams params) { 682 if (hasHintLabel()) { 683 return params.mHintLabelColor; 684 } 685 if (hasShiftedLetterHint()) { 686 return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor 687 : params.mShiftedLetterHintInactivatedColor; 688 } 689 return params.mHintLetterColor; 690 } 691 692 public final int selectMoreKeyTextSize(final KeyDrawParams params) { 693 return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize; 694 } 695 696 public final String getPreviewLabel() { 697 return isShiftedLetterActivated() ? mHintLabel : mLabel; 698 } 699 700 private boolean previewHasLetterSize() { 701 return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0 702 || StringUtils.codePointCount(getPreviewLabel()) == 1; 703 } 704 705 public final int selectPreviewTextSize(final KeyDrawParams params) { 706 if (previewHasLetterSize()) { 707 return params.mPreviewTextSize; 708 } 709 return params.mLetterSize; 710 } 711 712 @Nonnull 713 public Typeface selectPreviewTypeface(final KeyDrawParams params) { 714 if (previewHasLetterSize()) { 715 return selectTypeface(params); 716 } 717 return Typeface.DEFAULT_BOLD; 718 } 719 720 public final boolean isAlignHintLabelToBottom(final int defaultFlags) { 721 return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0; 722 } 723 724 public final boolean isAlignIconToBottom() { 725 return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0; 726 } 727 728 public final boolean isAlignLabelOffCenter() { 729 return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0; 730 } 731 732 public final boolean hasPopupHint() { 733 return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0; 734 } 735 736 public final boolean hasShiftedLetterHint() { 737 return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 738 && !TextUtils.isEmpty(mHintLabel); 739 } 740 741 public final boolean hasHintLabel() { 742 return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0; 743 } 744 745 public final boolean needsAutoXScale() { 746 return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; 747 } 748 749 public final boolean needsAutoScale() { 750 return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE; 751 } 752 753 public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) { 754 return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0; 755 } 756 757 public final boolean hasCustomActionLabel() { 758 return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0; 759 } 760 761 private final boolean isShiftedLetterActivated() { 762 return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0 763 && !TextUtils.isEmpty(mHintLabel); 764 } 765 766 public final int getMoreKeysColumnNumber() { 767 return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK; 768 } 769 770 public final boolean isMoreKeysFixedColumn() { 771 return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0; 772 } 773 774 public final boolean isMoreKeysFixedOrder() { 775 return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0; 776 } 777 778 public final boolean hasLabelsInMoreKeys() { 779 return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0; 780 } 781 782 public final int getMoreKeyLabelFlags() { 783 final int labelSizeFlag = hasLabelsInMoreKeys() 784 ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO 785 : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO; 786 return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE; 787 } 788 789 public final boolean needsDividersInMoreKeys() { 790 return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0; 791 } 792 793 public final boolean hasNoPanelAutoMoreKey() { 794 return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0; 795 } 796 797 @Nullable 798 public final String getOutputText() { 799 final OptionalAttributes attrs = mOptionalAttributes; 800 return (attrs != null) ? attrs.mOutputText : null; 801 } 802 803 public final int getAltCode() { 804 final OptionalAttributes attrs = mOptionalAttributes; 805 return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; 806 } 807 808 public int getIconId() { 809 return mIconId; 810 } 811 812 @Nullable 813 public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { 814 final OptionalAttributes attrs = mOptionalAttributes; 815 final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; 816 final int iconId = mEnabled ? getIconId() : disabledIconId; 817 final Drawable icon = iconSet.getIconDrawable(iconId); 818 if (icon != null) { 819 icon.setAlpha(alpha); 820 } 821 return icon; 822 } 823 824 @Nullable 825 public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) { 826 return iconSet.getIconDrawable(getIconId()); 827 } 828 829 /** 830 * Gets the width of the key in pixels, excluding the gap. 831 * @return The width of the key in pixels, excluding the gap. 832 */ 833 public int getWidth() { 834 return mWidth; 835 } 836 837 /** 838 * Gets the height of the key in pixels, excluding the gap. 839 * @return The height of the key in pixels, excluding the gap. 840 */ 841 public int getHeight() { 842 return mHeight; 843 } 844 845 /** 846 * The combined width in pixels of the horizontal gaps belonging to this key, both above and 847 * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key. 848 * @return Horizontal gap belonging to this key. 849 */ 850 public int getHorizontalGap() { 851 return mHorizontalGap; 852 } 853 854 /** 855 * The combined height in pixels of the vertical gaps belonging to this key, both above and 856 * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key. 857 * @return Vertical gap belonging to this key. 858 */ 859 public int getVerticalGap() { 860 return mVerticalGap; 861 } 862 863 /** 864 * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. 865 * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. 866 */ 867 public int getX() { 868 return mX; 869 } 870 871 /** 872 * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. 873 * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. 874 */ 875 public int getY() { 876 return mY; 877 } 878 879 public final int getDrawX() { 880 final int x = getX(); 881 final OptionalAttributes attrs = mOptionalAttributes; 882 return (attrs == null) ? x : x + attrs.mVisualInsetsLeft; 883 } 884 885 public final int getDrawWidth() { 886 final OptionalAttributes attrs = mOptionalAttributes; 887 return (attrs == null) ? mWidth 888 : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight; 889 } 890 891 /** 892 * Informs the key that it has been pressed, in case it needs to change its appearance or 893 * state. 894 * @see #onReleased() 895 */ 896 public void onPressed() { 897 mPressed = true; 898 } 899 900 /** 901 * Informs the key that it has been released, in case it needs to change its appearance or 902 * state. 903 * @see #onPressed() 904 */ 905 public void onReleased() { 906 mPressed = false; 907 } 908 909 public final boolean isEnabled() { 910 return mEnabled; 911 } 912 913 public void setEnabled(final boolean enabled) { 914 mEnabled = enabled; 915 } 916 917 @Nonnull 918 public Rect getHitBox() { 919 return mHitBox; 920 } 921 922 /** 923 * Detects if a point falls on this key. 924 * @param x the x-coordinate of the point 925 * @param y the y-coordinate of the point 926 * @return whether or not the point falls on the key. If the key is attached to an edge, it 927 * will assume that all points between the key and the edge are considered to be on the key. 928 * @see #markAsLeftEdge(KeyboardParams) etc. 929 */ 930 public boolean isOnKey(final int x, final int y) { 931 return mHitBox.contains(x, y); 932 } 933 934 /** 935 * Returns the square of the distance to the nearest edge of the key and the given point. 936 * @param x the x-coordinate of the point 937 * @param y the y-coordinate of the point 938 * @return the square of the distance of the point from the nearest edge of the key 939 */ 940 public int squaredDistanceToEdge(final int x, final int y) { 941 final int left = getX(); 942 final int right = left + mWidth; 943 final int top = getY(); 944 final int bottom = top + mHeight; 945 final int edgeX = x < left ? left : (x > right ? right : x); 946 final int edgeY = y < top ? top : (y > bottom ? bottom : y); 947 final int dx = x - edgeX; 948 final int dy = y - edgeY; 949 return dx * dx + dy * dy; 950 } 951 952 static class KeyBackgroundState { 953 private final int[] mReleasedState; 954 private final int[] mPressedState; 955 956 private KeyBackgroundState(final int ... attrs) { 957 mReleasedState = attrs; 958 mPressedState = Arrays.copyOf(attrs, attrs.length + 1); 959 mPressedState[attrs.length] = android.R.attr.state_pressed; 960 } 961 962 public int[] getState(final boolean pressed) { 963 return pressed ? mPressedState : mReleasedState; 964 } 965 966 public static final KeyBackgroundState[] STATES = { 967 // 0: BACKGROUND_TYPE_EMPTY 968 new KeyBackgroundState(android.R.attr.state_empty), 969 // 1: BACKGROUND_TYPE_NORMAL 970 new KeyBackgroundState(), 971 // 2: BACKGROUND_TYPE_FUNCTIONAL 972 new KeyBackgroundState(), 973 // 3: BACKGROUND_TYPE_STICKY_OFF 974 new KeyBackgroundState(android.R.attr.state_checkable), 975 // 4: BACKGROUND_TYPE_STICKY_ON 976 new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked), 977 // 5: BACKGROUND_TYPE_ACTION 978 new KeyBackgroundState(android.R.attr.state_active), 979 // 6: BACKGROUND_TYPE_SPACEBAR 980 new KeyBackgroundState(), 981 }; 982 } 983 984 /** 985 * Returns the background drawable for the key, based on the current state and type of the key. 986 * @return the background drawable of the key. 987 * @see android.graphics.drawable.StateListDrawable#setState(int[]) 988 */ 989 @Nonnull 990 public final Drawable selectBackgroundDrawable(@Nonnull final Drawable keyBackground, 991 @Nonnull final Drawable functionalKeyBackground, 992 @Nonnull final Drawable spacebarBackground) { 993 final Drawable background; 994 if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) { 995 background = functionalKeyBackground; 996 } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) { 997 background = spacebarBackground; 998 } else { 999 background = keyBackground; 1000 } 1001 final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed); 1002 background.setState(state); 1003 return background; 1004 } 1005 1006 public static class Spacer extends Key { 1007 public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle, 1008 final KeyboardParams params, final KeyboardRow row) { 1009 super(null /* keySpec */, keyAttr, keyStyle, params, row); 1010 } 1011 1012 /** 1013 * This constructor is being used only for divider in more keys keyboard. 1014 */ 1015 protected Spacer(final KeyboardParams params, final int x, final int y, final int width, 1016 final int height) { 1017 super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */, 1018 null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width, 1019 height, params.mHorizontalGap, params.mVerticalGap); 1020 } 1021 } 1022 } 1023