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