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