Home | History | Annotate | Download | only in keyboard
      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