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