| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.keyboard; |
| |
| import static com.android.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED; |
| import static com.android.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT; |
| import static com.android.inputmethod.latin.common.Constants.CODE_SHIFT; |
| import static com.android.inputmethod.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL; |
| import static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED; |
| |
| import android.content.res.TypedArray; |
| import android.graphics.Rect; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.text.TextUtils; |
| |
| import com.android.inputmethod.keyboard.internal.KeyDrawParams; |
| import com.android.inputmethod.keyboard.internal.KeySpecParser; |
| import com.android.inputmethod.keyboard.internal.KeyStyle; |
| import com.android.inputmethod.keyboard.internal.KeyVisualAttributes; |
| import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; |
| import com.android.inputmethod.keyboard.internal.KeyboardParams; |
| import com.android.inputmethod.keyboard.internal.KeyboardRow; |
| import com.android.inputmethod.keyboard.internal.MoreKeySpec; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.common.Constants; |
| import com.android.inputmethod.latin.common.StringUtils; |
| |
| import java.util.Arrays; |
| import java.util.Locale; |
| |
| import javax.annotation.Nonnull; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Class for describing the position and characteristics of a single key in the keyboard. |
| */ |
| public class Key implements Comparable<Key> { |
| /** |
| * The key code (unicode or custom code) that this key generates. |
| */ |
| private final int mCode; |
| |
| /** Label to display */ |
| private final String mLabel; |
| /** Hint label to display on the key in conjunction with the label */ |
| private final String mHintLabel; |
| /** Flags of the label */ |
| private final int mLabelFlags; |
| private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02; |
| private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04; |
| private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08; |
| // Font typeface specification. |
| private static final int LABEL_FLAGS_FONT_MASK = 0x30; |
| private static final int LABEL_FLAGS_FONT_NORMAL = 0x10; |
| private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20; |
| private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30; |
| // Start of key text ratio enum values |
| private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0; |
| private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40; |
| private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80; |
| private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0; |
| private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140; |
| // End of key text ratio mask enum values |
| private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200; |
| private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400; |
| private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800; |
| // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on |
| // and autoYScale bit is off, the key label may be shrunk only for X-direction. |
| // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled. |
| private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000; |
| private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000; |
| private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE |
| | LABEL_FLAGS_AUTO_Y_SCALE; |
| private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000; |
| private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000; |
| private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000; |
| private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000; |
| private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000; |
| private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000; |
| private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000; |
| |
| /** Icon to display instead of a label. Icon takes precedence over a label */ |
| private final int mIconId; |
| |
| /** Width of the key, excluding the gap */ |
| private final int mWidth; |
| /** Height of the key, excluding the gap */ |
| private final int mHeight; |
| /** |
| * The combined width in pixels of the horizontal gaps belonging to this key, both to the left |
| * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key. |
| */ |
| private final int mHorizontalGap; |
| /** |
| * The combined height in pixels of the vertical gaps belonging to this key, both above and |
| * below. I.e., mHeight + mVerticalGap = total height belonging to the key. |
| */ |
| private final int mVerticalGap; |
| /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ |
| private final int mX; |
| /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ |
| private final int mY; |
| /** Hit bounding box of the key */ |
| @Nonnull |
| private final Rect mHitBox = new Rect(); |
| |
| /** More keys. It is guaranteed that this is null or an array of one or more elements */ |
| @Nullable |
| private final MoreKeySpec[] mMoreKeys; |
| /** More keys column number and flags */ |
| private final int mMoreKeysColumnAndFlags; |
| private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff; |
| // If this flag is specified, more keys keyboard should have the specified number of columns. |
| // Otherwise more keys keyboard should have less than or equal to the specified maximum number |
| // of columns. |
| private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100; |
| // If this flag is specified, the order of more keys is determined by the order in the more |
| // keys' specification. Otherwise the order of more keys is automatically determined. |
| private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200; |
| private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0; |
| private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER = |
| MORE_KEYS_FLAGS_FIXED_COLUMN; |
| private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER = |
| (MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER); |
| private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000; |
| private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000; |
| private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000; |
| // TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively. |
| private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!"; |
| private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!"; |
| private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!"; |
| private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!"; |
| private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!"; |
| |
| /** Background type that represents different key background visual than normal one. */ |
| private final int mBackgroundType; |
| public static final int BACKGROUND_TYPE_EMPTY = 0; |
| public static final int BACKGROUND_TYPE_NORMAL = 1; |
| public static final int BACKGROUND_TYPE_FUNCTIONAL = 2; |
| public static final int BACKGROUND_TYPE_STICKY_OFF = 3; |
| public static final int BACKGROUND_TYPE_STICKY_ON = 4; |
| public static final int BACKGROUND_TYPE_ACTION = 5; |
| public static final int BACKGROUND_TYPE_SPACEBAR = 6; |
| |
| private final int mActionFlags; |
| private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01; |
| private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02; |
| private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04; |
| private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08; |
| |
| @Nullable |
| private final KeyVisualAttributes mKeyVisualAttributes; |
| @Nullable |
| private final OptionalAttributes mOptionalAttributes; |
| |
| private static final class OptionalAttributes { |
| /** Text to output when pressed. This can be multiple characters, like ".com" */ |
| public final String mOutputText; |
| public final int mAltCode; |
| /** Icon for disabled state */ |
| public final int mDisabledIconId; |
| /** The visual insets */ |
| public final int mVisualInsetsLeft; |
| public final int mVisualInsetsRight; |
| |
| private OptionalAttributes(final String outputText, final int altCode, |
| final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { |
| mOutputText = outputText; |
| mAltCode = altCode; |
| mDisabledIconId = disabledIconId; |
| mVisualInsetsLeft = visualInsetsLeft; |
| mVisualInsetsRight = visualInsetsRight; |
| } |
| |
| @Nullable |
| public static OptionalAttributes newInstance(final String outputText, final int altCode, |
| final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { |
| if (outputText == null && altCode == CODE_UNSPECIFIED |
| && disabledIconId == ICON_UNDEFINED && visualInsetsLeft == 0 |
| && visualInsetsRight == 0) { |
| return null; |
| } |
| return new OptionalAttributes(outputText, altCode, disabledIconId, visualInsetsLeft, |
| visualInsetsRight); |
| } |
| } |
| |
| private final int mHashCode; |
| |
| /** The current pressed state of this key */ |
| private boolean mPressed; |
| /** Key is enabled and responds on press */ |
| private boolean mEnabled = true; |
| |
| /** |
| * Constructor for a key on <code>MoreKeyKeyboard</code>, on <code>MoreSuggestions</code>, |
| * and in a <GridRows/>. |
| */ |
| public Key(@Nullable final String label, final int iconId, final int code, |
| @Nullable final String outputText, @Nullable final String hintLabel, |
| final int labelFlags, final int backgroundType, final int x, final int y, |
| final int width, final int height, final int horizontalGap, final int verticalGap) { |
| mWidth = width - horizontalGap; |
| mHeight = height - verticalGap; |
| mHorizontalGap = horizontalGap; |
| mVerticalGap = verticalGap; |
| mHintLabel = hintLabel; |
| mLabelFlags = labelFlags; |
| mBackgroundType = backgroundType; |
| // TODO: Pass keyActionFlags as an argument. |
| mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW; |
| mMoreKeys = null; |
| mMoreKeysColumnAndFlags = 0; |
| mLabel = label; |
| mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED, |
| ICON_UNDEFINED, 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */); |
| mCode = code; |
| mEnabled = (code != CODE_UNSPECIFIED); |
| mIconId = iconId; |
| // Horizontal gap is divided equally to both sides of the key. |
| mX = x + mHorizontalGap / 2; |
| mY = y; |
| mHitBox.set(x, y, x + width + 1, y + height); |
| mKeyVisualAttributes = null; |
| |
| mHashCode = computeHashCode(this); |
| } |
| |
| /** |
| * Create a key with the given top-left coordinate and extract its attributes from a key |
| * specification string, Key attribute array, key style, and etc. |
| * |
| * @param keySpec the key specification. |
| * @param keyAttr the Key XML attributes array. |
| * @param style the {@link KeyStyle} of this key. |
| * @param params the keyboard building parameters. |
| * @param row the row that this key belongs to. row's x-coordinate will be the right edge of |
| * this key. |
| */ |
| public Key(@Nullable final String keySpec, @Nonnull final TypedArray keyAttr, |
| @Nonnull final KeyStyle style, @Nonnull final KeyboardParams params, |
| @Nonnull final KeyboardRow row) { |
| mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap; |
| mVerticalGap = params.mVerticalGap; |
| |
| final float horizontalGapFloat = mHorizontalGap; |
| final int rowHeight = row.getRowHeight(); |
| mHeight = rowHeight - mVerticalGap; |
| |
| final float keyXPos = row.getKeyX(keyAttr); |
| final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); |
| final int keyYPos = row.getKeyY(); |
| |
| // Horizontal gap is divided equally to both sides of the key. |
| mX = Math.round(keyXPos + horizontalGapFloat / 2); |
| mY = keyYPos; |
| mWidth = Math.round(keyWidth - horizontalGapFloat); |
| mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, |
| keyYPos + rowHeight); |
| // Update row to have current x coordinate. |
| row.setXPos(keyXPos + keyWidth); |
| |
| mBackgroundType = style.getInt(keyAttr, |
| R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType()); |
| |
| final int baseWidth = params.mBaseWidth; |
| final int visualInsetsLeft = Math.round(keyAttr.getFraction( |
| R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0)); |
| final int visualInsetsRight = Math.round(keyAttr.getFraction( |
| R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0)); |
| |
| mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) |
| | row.getDefaultKeyLabelFlags(); |
| final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId); |
| final Locale localeForUpcasing = params.mId.getLocale(); |
| int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); |
| String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); |
| |
| // Get maximum column order number and set a relevant mode value. |
| int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER |
| | style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, |
| params.mMaxMoreKeysKeyboardColumn); |
| int value; |
| if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { |
| // Override with fixed column order number and set a relevant mode value. |
| moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER |
| | (value & MORE_KEYS_COLUMN_NUMBER_MASK); |
| } |
| if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { |
| // Override with fixed column order number and set a relevant mode value. |
| moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER |
| | (value & MORE_KEYS_COLUMN_NUMBER_MASK); |
| } |
| if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { |
| moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS; |
| } |
| if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { |
| moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; |
| } |
| if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { |
| moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; |
| } |
| mMoreKeysColumnAndFlags = moreKeysColumnAndFlags; |
| |
| final String[] additionalMoreKeys; |
| if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) { |
| additionalMoreKeys = null; |
| } else { |
| additionalMoreKeys = style.getStringArray(keyAttr, |
| R.styleable.Keyboard_Key_additionalMoreKeys); |
| } |
| moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); |
| if (moreKeys != null) { |
| actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; |
| mMoreKeys = new MoreKeySpec[moreKeys.length]; |
| for (int i = 0; i < moreKeys.length; i++) { |
| mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing); |
| } |
| } else { |
| mMoreKeys = null; |
| } |
| mActionFlags = actionFlags; |
| |
| mIconId = KeySpecParser.getIconId(keySpec); |
| final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, |
| R.styleable.Keyboard_Key_keyIconDisabled)); |
| |
| final int code = KeySpecParser.getCode(keySpec); |
| if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) { |
| mLabel = params.mId.mCustomActionLabel; |
| } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { |
| // This is a workaround to have a key that has a supplementary code point in its label. |
| // Because we can put a string in resource neither as a XML entity of a supplementary |
| // code point nor as a surrogate pair. |
| mLabel = new StringBuilder().appendCodePoint(code).toString(); |
| } else { |
| final String label = KeySpecParser.getLabel(keySpec); |
| mLabel = needsToUpcase |
| ? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing) |
| : label; |
| } |
| if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { |
| mHintLabel = null; |
| } else { |
| final String hintLabel = style.getString( |
| keyAttr, R.styleable.Keyboard_Key_keyHintLabel); |
| mHintLabel = needsToUpcase |
| ? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing) |
| : hintLabel; |
| } |
| String outputText = KeySpecParser.getOutputText(keySpec); |
| if (needsToUpcase) { |
| outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing); |
| } |
| // Choose the first letter of the label as primary code if not specified. |
| if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) |
| && !TextUtils.isEmpty(mLabel)) { |
| if (StringUtils.codePointCount(mLabel) == 1) { |
| // Use the first letter of the hint label if shiftedLetterActivated flag is |
| // specified. |
| if (hasShiftedLetterHint() && isShiftedLetterActivated()) { |
| mCode = mHintLabel.codePointAt(0); |
| } else { |
| mCode = mLabel.codePointAt(0); |
| } |
| } else { |
| // In some locale and case, the character might be represented by multiple code |
| // points, such as upper case Eszett of German alphabet. |
| outputText = mLabel; |
| mCode = CODE_OUTPUT_TEXT; |
| } |
| } else if (code == CODE_UNSPECIFIED && outputText != null) { |
| if (StringUtils.codePointCount(outputText) == 1) { |
| mCode = outputText.codePointAt(0); |
| outputText = null; |
| } else { |
| mCode = CODE_OUTPUT_TEXT; |
| } |
| } else { |
| mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing) |
| : code; |
| } |
| final int altCodeInAttr = KeySpecParser.parseCode( |
| style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); |
| final int altCode = needsToUpcase |
| ? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing) |
| : altCodeInAttr; |
| mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, |
| disabledIconId, visualInsetsLeft, visualInsetsRight); |
| mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); |
| mHashCode = computeHashCode(this); |
| } |
| |
| /** |
| * Copy constructor for DynamicGridKeyboard.GridKey. |
| * |
| * @param key the original key. |
| */ |
| protected Key(@Nonnull final Key key) { |
| this(key, key.mMoreKeys); |
| } |
| |
| private Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys) { |
| // Final attributes. |
| mCode = key.mCode; |
| mLabel = key.mLabel; |
| mHintLabel = key.mHintLabel; |
| mLabelFlags = key.mLabelFlags; |
| mIconId = key.mIconId; |
| mWidth = key.mWidth; |
| mHeight = key.mHeight; |
| mHorizontalGap = key.mHorizontalGap; |
| mVerticalGap = key.mVerticalGap; |
| mX = key.mX; |
| mY = key.mY; |
| mHitBox.set(key.mHitBox); |
| mMoreKeys = moreKeys; |
| mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags; |
| mBackgroundType = key.mBackgroundType; |
| mActionFlags = key.mActionFlags; |
| mKeyVisualAttributes = key.mKeyVisualAttributes; |
| mOptionalAttributes = key.mOptionalAttributes; |
| mHashCode = key.mHashCode; |
| // Key state. |
| mPressed = key.mPressed; |
| mEnabled = key.mEnabled; |
| } |
| |
| @Nonnull |
| public static Key removeRedundantMoreKeys(@Nonnull final Key key, |
| @Nonnull final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) { |
| final MoreKeySpec[] moreKeys = key.getMoreKeys(); |
| final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys( |
| moreKeys, lettersOnBaseLayout); |
| return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys); |
| } |
| |
| private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) { |
| if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false; |
| switch (keyboardElementId) { |
| case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: |
| case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: |
| case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: |
| case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| private static int computeHashCode(final Key key) { |
| return Arrays.hashCode(new Object[] { |
| key.mX, |
| key.mY, |
| key.mWidth, |
| key.mHeight, |
| key.mCode, |
| key.mLabel, |
| key.mHintLabel, |
| key.mIconId, |
| key.mBackgroundType, |
| Arrays.hashCode(key.mMoreKeys), |
| key.getOutputText(), |
| key.mActionFlags, |
| key.mLabelFlags, |
| // Key can be distinguishable without the following members. |
| // key.mOptionalAttributes.mAltCode, |
| // key.mOptionalAttributes.mDisabledIconId, |
| // key.mOptionalAttributes.mPreviewIconId, |
| // key.mHorizontalGap, |
| // key.mVerticalGap, |
| // key.mOptionalAttributes.mVisualInsetLeft, |
| // key.mOptionalAttributes.mVisualInsetRight, |
| // key.mMaxMoreKeysColumn, |
| }); |
| } |
| |
| private boolean equalsInternal(final Key o) { |
| if (this == o) return true; |
| return o.mX == mX |
| && o.mY == mY |
| && o.mWidth == mWidth |
| && o.mHeight == mHeight |
| && o.mCode == mCode |
| && TextUtils.equals(o.mLabel, mLabel) |
| && TextUtils.equals(o.mHintLabel, mHintLabel) |
| && o.mIconId == mIconId |
| && o.mBackgroundType == mBackgroundType |
| && Arrays.equals(o.mMoreKeys, mMoreKeys) |
| && TextUtils.equals(o.getOutputText(), getOutputText()) |
| && o.mActionFlags == mActionFlags |
| && o.mLabelFlags == mLabelFlags; |
| } |
| |
| @Override |
| public int compareTo(Key o) { |
| if (equalsInternal(o)) return 0; |
| if (mHashCode > o.mHashCode) return 1; |
| return -1; |
| } |
| |
| @Override |
| public int hashCode() { |
| return mHashCode; |
| } |
| |
| @Override |
| public boolean equals(final Object o) { |
| return o instanceof Key && equalsInternal((Key)o); |
| } |
| |
| @Override |
| public String toString() { |
| return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight(); |
| } |
| |
| public String toShortString() { |
| final int code = getCode(); |
| if (code == Constants.CODE_OUTPUT_TEXT) { |
| return getOutputText(); |
| } |
| return Constants.printableCode(code); |
| } |
| |
| public String toLongString() { |
| final int iconId = getIconId(); |
| final String topVisual = (iconId == KeyboardIconsSet.ICON_UNDEFINED) |
| ? KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(iconId) : getLabel(); |
| final String hintLabel = getHintLabel(); |
| final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel; |
| return toString() + " " + visual + "/" + backgroundName(mBackgroundType); |
| } |
| |
| private static String backgroundName(final int backgroundType) { |
| switch (backgroundType) { |
| case BACKGROUND_TYPE_EMPTY: return "empty"; |
| case BACKGROUND_TYPE_NORMAL: return "normal"; |
| case BACKGROUND_TYPE_FUNCTIONAL: return "functional"; |
| case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff"; |
| case BACKGROUND_TYPE_STICKY_ON: return "stickyOn"; |
| case BACKGROUND_TYPE_ACTION: return "action"; |
| case BACKGROUND_TYPE_SPACEBAR: return "spacebar"; |
| default: return null; |
| } |
| } |
| |
| public int getCode() { |
| return mCode; |
| } |
| |
| @Nullable |
| public String getLabel() { |
| return mLabel; |
| } |
| |
| @Nullable |
| public String getHintLabel() { |
| return mHintLabel; |
| } |
| |
| @Nullable |
| public MoreKeySpec[] getMoreKeys() { |
| return mMoreKeys; |
| } |
| |
| public void markAsLeftEdge(final KeyboardParams params) { |
| mHitBox.left = params.mLeftPadding; |
| } |
| |
| public void markAsRightEdge(final KeyboardParams params) { |
| mHitBox.right = params.mOccupiedWidth - params.mRightPadding; |
| } |
| |
| public void markAsTopEdge(final KeyboardParams params) { |
| mHitBox.top = params.mTopPadding; |
| } |
| |
| public void markAsBottomEdge(final KeyboardParams params) { |
| mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding; |
| } |
| |
| public final boolean isSpacer() { |
| return this instanceof Spacer; |
| } |
| |
| public final boolean isActionKey() { |
| return mBackgroundType == BACKGROUND_TYPE_ACTION; |
| } |
| |
| public final boolean isShift() { |
| return mCode == CODE_SHIFT; |
| } |
| |
| public final boolean isModifier() { |
| return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL; |
| } |
| |
| public final boolean isRepeatable() { |
| return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0; |
| } |
| |
| public final boolean noKeyPreview() { |
| return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0; |
| } |
| |
| public final boolean altCodeWhileTyping() { |
| return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0; |
| } |
| |
| public final boolean isLongPressEnabled() { |
| // We need not start long press timer on the key which has activated shifted letter. |
| return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0 |
| && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0; |
| } |
| |
| public KeyVisualAttributes getVisualAttributes() { |
| return mKeyVisualAttributes; |
| } |
| |
| @Nonnull |
| public final Typeface selectTypeface(final KeyDrawParams params) { |
| switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) { |
| case LABEL_FLAGS_FONT_NORMAL: |
| return Typeface.DEFAULT; |
| case LABEL_FLAGS_FONT_MONO_SPACE: |
| return Typeface.MONOSPACE; |
| case LABEL_FLAGS_FONT_DEFAULT: |
| default: |
| // The type-face is specified by keyTypeface attribute. |
| return params.mTypeface; |
| } |
| } |
| |
| public final int selectTextSize(final KeyDrawParams params) { |
| switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) { |
| case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO: |
| return params.mLetterSize; |
| case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO: |
| return params.mLargeLetterSize; |
| case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO: |
| return params.mLabelSize; |
| case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO: |
| return params.mHintLabelSize; |
| default: // No follow key ratio flag specified. |
| return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize; |
| } |
| } |
| |
| public final int selectTextColor(final KeyDrawParams params) { |
| if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { |
| return params.mFunctionalTextColor; |
| } |
| return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; |
| } |
| |
| public final int selectHintTextSize(final KeyDrawParams params) { |
| if (hasHintLabel()) { |
| return params.mHintLabelSize; |
| } |
| if (hasShiftedLetterHint()) { |
| return params.mShiftedLetterHintSize; |
| } |
| return params.mHintLetterSize; |
| } |
| |
| public final int selectHintTextColor(final KeyDrawParams params) { |
| if (hasHintLabel()) { |
| return params.mHintLabelColor; |
| } |
| if (hasShiftedLetterHint()) { |
| return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor |
| : params.mShiftedLetterHintInactivatedColor; |
| } |
| return params.mHintLetterColor; |
| } |
| |
| public final int selectMoreKeyTextSize(final KeyDrawParams params) { |
| return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize; |
| } |
| |
| public final String getPreviewLabel() { |
| return isShiftedLetterActivated() ? mHintLabel : mLabel; |
| } |
| |
| private boolean previewHasLetterSize() { |
| return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0 |
| || StringUtils.codePointCount(getPreviewLabel()) == 1; |
| } |
| |
| public final int selectPreviewTextSize(final KeyDrawParams params) { |
| if (previewHasLetterSize()) { |
| return params.mPreviewTextSize; |
| } |
| return params.mLetterSize; |
| } |
| |
| @Nonnull |
| public Typeface selectPreviewTypeface(final KeyDrawParams params) { |
| if (previewHasLetterSize()) { |
| return selectTypeface(params); |
| } |
| return Typeface.DEFAULT_BOLD; |
| } |
| |
| public final boolean isAlignHintLabelToBottom(final int defaultFlags) { |
| return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0; |
| } |
| |
| public final boolean isAlignIconToBottom() { |
| return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0; |
| } |
| |
| public final boolean isAlignLabelOffCenter() { |
| return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0; |
| } |
| |
| public final boolean hasPopupHint() { |
| return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0; |
| } |
| |
| public final boolean hasShiftedLetterHint() { |
| return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 |
| && !TextUtils.isEmpty(mHintLabel); |
| } |
| |
| public final boolean hasHintLabel() { |
| return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0; |
| } |
| |
| public final boolean needsAutoXScale() { |
| return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; |
| } |
| |
| public final boolean needsAutoScale() { |
| return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE; |
| } |
| |
| public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) { |
| return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0; |
| } |
| |
| public final boolean hasCustomActionLabel() { |
| return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0; |
| } |
| |
| private final boolean isShiftedLetterActivated() { |
| return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0 |
| && !TextUtils.isEmpty(mHintLabel); |
| } |
| |
| public final int getMoreKeysColumnNumber() { |
| return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK; |
| } |
| |
| public final boolean isMoreKeysFixedColumn() { |
| return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0; |
| } |
| |
| public final boolean isMoreKeysFixedOrder() { |
| return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0; |
| } |
| |
| public final boolean hasLabelsInMoreKeys() { |
| return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0; |
| } |
| |
| public final int getMoreKeyLabelFlags() { |
| final int labelSizeFlag = hasLabelsInMoreKeys() |
| ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO |
| : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO; |
| return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE; |
| } |
| |
| public final boolean needsDividersInMoreKeys() { |
| return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0; |
| } |
| |
| public final boolean hasNoPanelAutoMoreKey() { |
| return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0; |
| } |
| |
| @Nullable |
| public final String getOutputText() { |
| final OptionalAttributes attrs = mOptionalAttributes; |
| return (attrs != null) ? attrs.mOutputText : null; |
| } |
| |
| public final int getAltCode() { |
| final OptionalAttributes attrs = mOptionalAttributes; |
| return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; |
| } |
| |
| public int getIconId() { |
| return mIconId; |
| } |
| |
| @Nullable |
| public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { |
| final OptionalAttributes attrs = mOptionalAttributes; |
| final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; |
| final int iconId = mEnabled ? getIconId() : disabledIconId; |
| final Drawable icon = iconSet.getIconDrawable(iconId); |
| if (icon != null) { |
| icon.setAlpha(alpha); |
| } |
| return icon; |
| } |
| |
| @Nullable |
| public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) { |
| return iconSet.getIconDrawable(getIconId()); |
| } |
| |
| /** |
| * Gets the width of the key in pixels, excluding the gap. |
| * @return The width of the key in pixels, excluding the gap. |
| */ |
| public int getWidth() { |
| return mWidth; |
| } |
| |
| /** |
| * Gets the height of the key in pixels, excluding the gap. |
| * @return The height of the key in pixels, excluding the gap. |
| */ |
| public int getHeight() { |
| return mHeight; |
| } |
| |
| /** |
| * The combined width in pixels of the horizontal gaps belonging to this key, both above and |
| * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key. |
| * @return Horizontal gap belonging to this key. |
| */ |
| public int getHorizontalGap() { |
| return mHorizontalGap; |
| } |
| |
| /** |
| * The combined height in pixels of the vertical gaps belonging to this key, both above and |
| * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key. |
| * @return Vertical gap belonging to this key. |
| */ |
| public int getVerticalGap() { |
| return mVerticalGap; |
| } |
| |
| /** |
| * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. |
| * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. |
| */ |
| public int getX() { |
| return mX; |
| } |
| |
| /** |
| * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. |
| * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. |
| */ |
| public int getY() { |
| return mY; |
| } |
| |
| public final int getDrawX() { |
| final int x = getX(); |
| final OptionalAttributes attrs = mOptionalAttributes; |
| return (attrs == null) ? x : x + attrs.mVisualInsetsLeft; |
| } |
| |
| public final int getDrawWidth() { |
| final OptionalAttributes attrs = mOptionalAttributes; |
| return (attrs == null) ? mWidth |
| : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight; |
| } |
| |
| /** |
| * Informs the key that it has been pressed, in case it needs to change its appearance or |
| * state. |
| * @see #onReleased() |
| */ |
| public void onPressed() { |
| mPressed = true; |
| } |
| |
| /** |
| * Informs the key that it has been released, in case it needs to change its appearance or |
| * state. |
| * @see #onPressed() |
| */ |
| public void onReleased() { |
| mPressed = false; |
| } |
| |
| public final boolean isEnabled() { |
| return mEnabled; |
| } |
| |
| public void setEnabled(final boolean enabled) { |
| mEnabled = enabled; |
| } |
| |
| @Nonnull |
| public Rect getHitBox() { |
| return mHitBox; |
| } |
| |
| /** |
| * Detects if a point falls on this key. |
| * @param x the x-coordinate of the point |
| * @param y the y-coordinate of the point |
| * @return whether or not the point falls on the key. If the key is attached to an edge, it |
| * will assume that all points between the key and the edge are considered to be on the key. |
| * @see #markAsLeftEdge(KeyboardParams) etc. |
| */ |
| public boolean isOnKey(final int x, final int y) { |
| return mHitBox.contains(x, y); |
| } |
| |
| /** |
| * Returns the square of the distance to the nearest edge of the key and the given point. |
| * @param x the x-coordinate of the point |
| * @param y the y-coordinate of the point |
| * @return the square of the distance of the point from the nearest edge of the key |
| */ |
| public int squaredDistanceToEdge(final int x, final int y) { |
| final int left = getX(); |
| final int right = left + mWidth; |
| final int top = getY(); |
| final int bottom = top + mHeight; |
| final int edgeX = x < left ? left : (x > right ? right : x); |
| final int edgeY = y < top ? top : (y > bottom ? bottom : y); |
| final int dx = x - edgeX; |
| final int dy = y - edgeY; |
| return dx * dx + dy * dy; |
| } |
| |
| static class KeyBackgroundState { |
| private final int[] mReleasedState; |
| private final int[] mPressedState; |
| |
| private KeyBackgroundState(final int ... attrs) { |
| mReleasedState = attrs; |
| mPressedState = Arrays.copyOf(attrs, attrs.length + 1); |
| mPressedState[attrs.length] = android.R.attr.state_pressed; |
| } |
| |
| public int[] getState(final boolean pressed) { |
| return pressed ? mPressedState : mReleasedState; |
| } |
| |
| public static final KeyBackgroundState[] STATES = { |
| // 0: BACKGROUND_TYPE_EMPTY |
| new KeyBackgroundState(android.R.attr.state_empty), |
| // 1: BACKGROUND_TYPE_NORMAL |
| new KeyBackgroundState(), |
| // 2: BACKGROUND_TYPE_FUNCTIONAL |
| new KeyBackgroundState(), |
| // 3: BACKGROUND_TYPE_STICKY_OFF |
| new KeyBackgroundState(android.R.attr.state_checkable), |
| // 4: BACKGROUND_TYPE_STICKY_ON |
| new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked), |
| // 5: BACKGROUND_TYPE_ACTION |
| new KeyBackgroundState(android.R.attr.state_active), |
| // 6: BACKGROUND_TYPE_SPACEBAR |
| new KeyBackgroundState(), |
| }; |
| } |
| |
| /** |
| * Returns the background drawable for the key, based on the current state and type of the key. |
| * @return the background drawable of the key. |
| * @see android.graphics.drawable.StateListDrawable#setState(int[]) |
| */ |
| @Nonnull |
| public final Drawable selectBackgroundDrawable(@Nonnull final Drawable keyBackground, |
| @Nonnull final Drawable functionalKeyBackground, |
| @Nonnull final Drawable spacebarBackground) { |
| final Drawable background; |
| if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) { |
| background = functionalKeyBackground; |
| } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) { |
| background = spacebarBackground; |
| } else { |
| background = keyBackground; |
| } |
| final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed); |
| background.setState(state); |
| return background; |
| } |
| |
| public static class Spacer extends Key { |
| public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle, |
| final KeyboardParams params, final KeyboardRow row) { |
| super(null /* keySpec */, keyAttr, keyStyle, params, row); |
| } |
| |
| /** |
| * This constructor is being used only for divider in more keys keyboard. |
| */ |
| protected Spacer(final KeyboardParams params, final int x, final int y, final int width, |
| final int height) { |
| super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */, |
| null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width, |
| height, params.mHorizontalGap, params.mVerticalGap); |
| } |
| } |
| } |