Key.java revision b118d4cb58c27131f6333ada281c772edfcaa74b
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import android.content.res.Resources;
20import android.content.res.TypedArray;
21import android.content.res.XmlResourceParser;
22import android.graphics.Typeface;
23import android.graphics.drawable.Drawable;
24import android.text.TextUtils;
25import android.util.Xml;
26
27import com.android.inputmethod.keyboard.internal.KeyStyles;
28import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
29import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
30import com.android.inputmethod.keyboard.internal.KeyboardParser;
31import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException;
32import com.android.inputmethod.keyboard.internal.PopupCharactersParser;
33import com.android.inputmethod.keyboard.internal.Row;
34import com.android.inputmethod.latin.R;
35
36import java.util.HashMap;
37import java.util.Map;
38
39/**
40 * Class for describing the position and characteristics of a single key in the keyboard.
41 */
42public class Key {
43    /**
44     * The key code (unicode or custom code) that this key generates.
45     */
46    public final int mCode;
47
48    /** Label to display */
49    public final CharSequence mLabel;
50    /** Hint label to display on the key in conjunction with the label */
51    public final CharSequence mHintLabel;
52    /** Option of the label */
53    public final int mLabelOption;
54    public static final int LABEL_OPTION_ALIGN_LEFT = 0x01;
55    public static final int LABEL_OPTION_ALIGN_RIGHT = 0x02;
56    public static final int LABEL_OPTION_ALIGN_LEFT_OF_CENTER = 0x08;
57    private static final int LABEL_OPTION_LARGE_LETTER = 0x10;
58    private static final int LABEL_OPTION_FONT_NORMAL = 0x20;
59    private static final int LABEL_OPTION_FONT_MONO_SPACE = 0x40;
60    private static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x80;
61    private static final int LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO = 0x100;
62    private static final int LABEL_OPTION_HAS_POPUP_HINT = 0x200;
63    private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x400;
64    private static final int LABEL_OPTION_HAS_HINT_LABEL = 0x800;
65
66    /** Icon to display instead of a label. Icon takes precedence over a label */
67    private Drawable mIcon;
68    /** Preview version of the icon, for the preview popup */
69    private Drawable mPreviewIcon;
70
71    /** Width of the key, not including the gap */
72    public final int mWidth;
73    /** Height of the key, not including the gap */
74    public final int mHeight;
75    /** The horizontal gap around this key */
76    public final int mGap;
77    /** The visual insets */
78    public final int mVisualInsetsLeft;
79    public final int mVisualInsetsRight;
80    /** Whether this key is sticky, i.e., a toggle key */
81    public final boolean mSticky;
82    /** X coordinate of the key in the keyboard layout */
83    public final int mX;
84    /** Y coordinate of the key in the keyboard layout */
85    public final int mY;
86    /** Text to output when pressed. This can be multiple characters, like ".com" */
87    public final CharSequence mOutputText;
88    /** Popup characters */
89    public final CharSequence[] mPopupCharacters;
90    /** Popup keyboard maximum column number */
91    public final int mMaxPopupColumn;
92
93    /**
94     * Flags that specify the anchoring to edges of the keyboard for detecting touch events
95     * that are just out of the boundary of the key. This is a bit mask of
96     * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
97     * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
98     */
99    private int mEdgeFlags;
100    /** Whether this is a functional key which has different key top than normal key */
101    public final boolean mFunctional;
102    /** Whether this key repeats itself when held down */
103    public final boolean mRepeatable;
104
105    /** The Keyboard that this key belongs to */
106    private final Keyboard mKeyboard;
107
108    /** The current pressed state of this key */
109    private boolean mPressed;
110    /** If this is a sticky key, is its highlight on? */
111    private boolean mHighlightOn;
112    /** Key is enabled and responds on press */
113    private boolean mEnabled = true;
114
115    // keyWidth constants
116    private static final int KEYWIDTH_FILL_RIGHT = 0;
117    private static final int KEYWIDTH_FILL_BOTH = -1;
118
119    private final static int[] KEY_STATE_NORMAL_ON = {
120        android.R.attr.state_checkable,
121        android.R.attr.state_checked
122    };
123
124    private final static int[] KEY_STATE_PRESSED_ON = {
125        android.R.attr.state_pressed,
126        android.R.attr.state_checkable,
127        android.R.attr.state_checked
128    };
129
130    private final static int[] KEY_STATE_NORMAL_OFF = {
131        android.R.attr.state_checkable
132    };
133
134    private final static int[] KEY_STATE_PRESSED_OFF = {
135        android.R.attr.state_pressed,
136        android.R.attr.state_checkable
137    };
138
139    private final static int[] KEY_STATE_NORMAL = {
140    };
141
142    private final static int[] KEY_STATE_PRESSED = {
143        android.R.attr.state_pressed
144    };
145
146    // functional normal state (with properties)
147    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
148            android.R.attr.state_single
149    };
150
151    // functional pressed state (with properties)
152    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
153            android.R.attr.state_single,
154            android.R.attr.state_pressed
155    };
156
157    // RTL parenthesis character swapping map.
158    private static final Map<Integer, Integer> sRtlParenthesisMap = new HashMap<Integer, Integer>();
159
160    static {
161        // The all letters need to be mirrored are found at
162        // http://www.unicode.org/Public/6.0.0/ucd/extracted/DerivedBinaryProperties.txt
163        addRtlParenthesisPair('(', ')');
164        addRtlParenthesisPair('[', ']');
165        addRtlParenthesisPair('{', '}');
166        addRtlParenthesisPair('<', '>');
167        // \u00ab: LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
168        // \u00bb: RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
169        addRtlParenthesisPair('\u00ab', '\u00bb');
170        // \u2039: SINGLE LEFT-POINTING ANGLE QUOTATION MARK
171        // \u203a: SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
172        addRtlParenthesisPair('\u2039', '\u203a');
173        // \u2264: LESS-THAN OR EQUAL TO
174        // \u2265: GREATER-THAN OR EQUAL TO
175        addRtlParenthesisPair('\u2264', '\u2265');
176    }
177
178    private static void addRtlParenthesisPair(int left, int right) {
179        sRtlParenthesisMap.put(left, right);
180        sRtlParenthesisMap.put(right, left);
181    }
182
183    public static int getRtlParenthesisCode(int code) {
184        if (sRtlParenthesisMap.containsKey(code)) {
185            return sRtlParenthesisMap.get(code);
186        } else {
187            return code;
188        }
189    }
190
191    /**
192     * This constructor is being used only for key in popup mini keyboard.
193     */
194    public Key(Resources res, Keyboard keyboard, CharSequence popupCharacter, int x, int y,
195            int width, int height, int edgeFlags) {
196        mKeyboard = keyboard;
197        mHeight = height - keyboard.getVerticalGap();
198        mGap = keyboard.getHorizontalGap();
199        mVisualInsetsLeft = mVisualInsetsRight = 0;
200        mWidth = width - mGap;
201        mEdgeFlags = edgeFlags;
202        mHintLabel = null;
203        mLabelOption = 0;
204        mFunctional = false;
205        mSticky = false;
206        mRepeatable = false;
207        mPopupCharacters = null;
208        mMaxPopupColumn = 0;
209        final String popupSpecification = popupCharacter.toString();
210        mLabel = PopupCharactersParser.getLabel(popupSpecification);
211        mOutputText = PopupCharactersParser.getOutputText(popupSpecification);
212        final int code = PopupCharactersParser.getCode(res, popupSpecification);
213        mCode = keyboard.isRtlKeyboard() ? getRtlParenthesisCode(code) : code;
214        mIcon = keyboard.mIconsSet.getIcon(PopupCharactersParser.getIconId(popupSpecification));
215        // Horizontal gap is divided equally to both sides of the key.
216        mX = x + mGap / 2;
217        mY = y;
218    }
219
220    /**
221     * Create a key with the given top-left coordinate and extract its attributes from the XML
222     * parser.
223     * @param res resources associated with the caller's context
224     * @param row the row that this key belongs to. The row must already be attached to
225     * a {@link Keyboard}.
226     * @param x the x coordinate of the top-left
227     * @param y the y coordinate of the top-left
228     * @param parser the XML parser containing the attributes for this key
229     * @param keyStyles active key styles set
230     */
231    public Key(Resources res, Row row, int x, int y, XmlResourceParser parser,
232            KeyStyles keyStyles) {
233        mKeyboard = row.getKeyboard();
234
235        final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
236                R.styleable.Keyboard);
237        int keyWidth;
238        try {
239            mHeight = KeyboardParser.getDimensionOrFraction(keyboardAttr,
240                    R.styleable.Keyboard_rowHeight,
241                    mKeyboard.getKeyboardHeight(), row.mDefaultHeight) - row.mVerticalGap;
242            mGap = KeyboardParser.getDimensionOrFraction(keyboardAttr,
243                    R.styleable.Keyboard_horizontalGap,
244                    mKeyboard.getDisplayWidth(), row.mDefaultHorizontalGap);
245            keyWidth = KeyboardParser.getDimensionOrFraction(keyboardAttr,
246                    R.styleable.Keyboard_keyWidth,
247                    mKeyboard.getDisplayWidth(), row.mDefaultWidth);
248        } finally {
249            keyboardAttr.recycle();
250        }
251
252        final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
253                R.styleable.Keyboard_Key);
254        try {
255            final KeyStyle style;
256            if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
257                String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
258                style = keyStyles.getKeyStyle(styleName);
259                if (style == null)
260                    throw new ParseException("Unknown key style: " + styleName, parser);
261            } else {
262                style = keyStyles.getEmptyKeyStyle();
263            }
264
265            final int keyboardWidth = mKeyboard.getDisplayWidth();
266            int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr,
267                    R.styleable.Keyboard_Key_keyXPos, keyboardWidth, x);
268            if (keyXPos < 0) {
269                // If keyXPos is negative, the actual x-coordinate will be k + keyXPos.
270                keyXPos += keyboardWidth;
271                if (keyXPos < x) {
272                    // keyXPos shouldn't be less than x because drawable area for this key starts
273                    // at x. Or, this key will overlaps the adjacent key on its left hand side.
274                    keyXPos = x;
275                }
276            }
277            if (keyWidth == KEYWIDTH_FILL_RIGHT) {
278                // If keyWidth is zero, the actual key width will be determined to fill out the
279                // area up to the right edge of the keyboard.
280                keyWidth = keyboardWidth - keyXPos;
281            } else if (keyWidth <= KEYWIDTH_FILL_BOTH) {
282                // If keyWidth is negative, the actual key width will be determined to fill out the
283                // area between the nearest key on the left hand side and the right edge of the
284                // keyboard.
285                keyXPos = x;
286                keyWidth = keyboardWidth - keyXPos;
287            }
288
289            // Horizontal gap is divided equally to both sides of the key.
290            mX = keyXPos + mGap / 2;
291            mY = y;
292            mWidth = keyWidth - mGap;
293
294            CharSequence[] popupCharacters = style.getTextArray(
295                    keyAttr, R.styleable.Keyboard_Key_popupCharacters);
296            if (mKeyboard.mId.mPasswordInput) {
297                popupCharacters = PopupCharactersParser.filterOut(
298                        res, popupCharacters, PopupCharactersParser.NON_ASCII_FILTER);
299            }
300            // In Arabic symbol layouts, we'd like to keep digits in popup characters regardless of
301            // config_digit_popup_characters_enabled.
302            if (mKeyboard.mId.isAlphabetKeyboard() && !res.getBoolean(
303                    R.bool.config_digit_popup_characters_enabled)) {
304                mPopupCharacters = PopupCharactersParser.filterOut(
305                        res, popupCharacters, PopupCharactersParser.DIGIT_FILTER);
306            } else {
307                mPopupCharacters = popupCharacters;
308            }
309            mMaxPopupColumn = style.getInt(keyboardAttr,
310                    R.styleable.Keyboard_Key_maxPopupKeyboardColumn,
311                    mKeyboard.getMaxPopupKeyboardColumn());
312
313            mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false);
314            mFunctional = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional, false);
315            mSticky = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky, false);
316            mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true);
317            mEdgeFlags = 0;
318
319            final KeyboardIconsSet iconsSet = mKeyboard.mIconsSet;
320            mVisualInsetsLeft = KeyboardParser.getDimensionOrFraction(keyAttr,
321                    R.styleable.Keyboard_Key_visualInsetsLeft, keyboardWidth, 0);
322            mVisualInsetsRight = KeyboardParser.getDimensionOrFraction(keyAttr,
323                    R.styleable.Keyboard_Key_visualInsetsRight, keyboardWidth, 0);
324            mPreviewIcon = iconsSet.getIcon(style.getInt(
325                    keyAttr, R.styleable.Keyboard_Key_keyIconPreview,
326                    KeyboardIconsSet.ICON_UNDEFINED));
327            mIcon = iconsSet.getIcon(style.getInt(
328                    keyAttr, R.styleable.Keyboard_Key_keyIcon,
329                    KeyboardIconsSet.ICON_UNDEFINED));
330            final int shiftedIconId = style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted,
331                    KeyboardIconsSet.ICON_UNDEFINED);
332            if (shiftedIconId != KeyboardIconsSet.ICON_UNDEFINED) {
333                final Drawable shiftedIcon = iconsSet.getIcon(shiftedIconId);
334                mKeyboard.addShiftedIcon(this, shiftedIcon);
335            }
336            mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
337
338            mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
339            mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0);
340            mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
341            // Choose the first letter of the label as primary code if not
342            // specified.
343            final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
344                    Keyboard.CODE_UNSPECIFIED);
345            if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) {
346                final int firstChar = mLabel.charAt(0);
347                mCode = mKeyboard.isRtlKeyboard() ? getRtlParenthesisCode(firstChar) : firstChar;
348            } else if (code != Keyboard.CODE_UNSPECIFIED) {
349                mCode = code;
350            } else {
351                mCode = Keyboard.CODE_DUMMY;
352            }
353            if (mCode == Keyboard.CODE_SHIFT) {
354                mKeyboard.addShiftKey(this);
355            }
356        } finally {
357            keyAttr.recycle();
358        }
359    }
360
361    public void addEdgeFlags(int flags) {
362        mEdgeFlags |= flags;
363    }
364
365    public CharSequence getCaseAdjustedLabel() {
366        return mKeyboard.adjustLabelCase(mLabel);
367    }
368
369    public Typeface selectTypeface(Typeface defaultTypeface) {
370        // TODO: Handle "bold" here too?
371        if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) {
372            return Typeface.DEFAULT;
373        } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) {
374            return Typeface.MONOSPACE;
375        } else {
376            return defaultTypeface;
377        }
378    }
379
380    public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) {
381        if (mLabel.length() > 1
382                && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO
383                        | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) {
384            return label;
385        } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) {
386            return hintLabel;
387        } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) {
388            return largeLetter;
389        } else {
390            return letter;
391        }
392    }
393
394    public boolean hasPopupHint() {
395        return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0;
396    }
397
398    public boolean hasUppercaseLetter() {
399        return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0;
400    }
401
402    public boolean hasHintLabel() {
403        return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0;
404    }
405
406    public Drawable getIcon() {
407        return mIcon;
408    }
409
410    public Drawable getPreviewIcon() {
411        return mPreviewIcon;
412    }
413
414    public void setIcon(Drawable icon) {
415        mIcon = icon;
416    }
417
418    public void setPreviewIcon(Drawable icon) {
419        mPreviewIcon = icon;
420    }
421
422    /**
423     * Informs the key that it has been pressed, in case it needs to change its appearance or
424     * state.
425     * @see #onReleased()
426     */
427    public void onPressed() {
428        mPressed = true;
429    }
430
431    /**
432     * Informs the key that it has been released, in case it needs to change its appearance or
433     * state.
434     * @see #onPressed()
435     */
436    public void onReleased() {
437        mPressed = false;
438    }
439
440    public void setHighlightOn(boolean highlightOn) {
441        mHighlightOn = highlightOn;
442    }
443
444    public boolean isEnabled() {
445        return mEnabled;
446    }
447
448    public void setEnabled(boolean enabled) {
449        mEnabled = enabled;
450    }
451
452    /**
453     * Detects if a point falls on this key.
454     * @param x the x-coordinate of the point
455     * @param y the y-coordinate of the point
456     * @return whether or not the point falls on the key. If the key is attached to an edge, it will
457     * assume that all points between the key and the edge are considered to be on the key.
458     */
459    public boolean isOnKey(int x, int y) {
460        final int left = mX - mGap / 2;
461        final int right = left + mWidth + mGap;
462        final int top = mY;
463        final int bottom = top + mHeight + mKeyboard.getVerticalGap();
464        final int flags = mEdgeFlags;
465        if (flags == 0) {
466            return x >= left && x <= right && y >= top && y <= bottom;
467        }
468        final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
469        final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
470        final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
471        final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
472        // In order to mitigate rounding errors, we use (left <= x <= right) here.
473        return (x >= left || leftEdge) && (x <= right || rightEdge)
474                && (y >= top || topEdge) && (y <= bottom || bottomEdge);
475    }
476
477    /**
478     * Returns the square of the distance to the nearest edge of the key and the given point.
479     * @param x the x-coordinate of the point
480     * @param y the y-coordinate of the point
481     * @return the square of the distance of the point from the nearest edge of the key
482     */
483    public int squaredDistanceToEdge(int x, int y) {
484        final int left = mX;
485        final int right = left + mWidth;
486        final int top = mY;
487        final int bottom = top + mHeight;
488        final int edgeX = x < left ? left : (x > right ? right : x);
489        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
490        final int dx = x - edgeX;
491        final int dy = y - edgeY;
492        return dx * dx + dy * dy;
493    }
494
495    /**
496     * Returns the drawable state for the key, based on the current state and type of the key.
497     * @return the drawable state of the key.
498     * @see android.graphics.drawable.StateListDrawable#setState(int[])
499     */
500    public int[] getCurrentDrawableState() {
501        final boolean pressed = mPressed;
502        if (!mSticky && mFunctional) {
503            if (pressed) {
504                return KEY_STATE_FUNCTIONAL_PRESSED;
505            } else {
506                return KEY_STATE_FUNCTIONAL_NORMAL;
507            }
508        }
509
510        int[] states = KEY_STATE_NORMAL;
511
512        if (mHighlightOn) {
513            if (pressed) {
514                states = KEY_STATE_PRESSED_ON;
515            } else {
516                states = KEY_STATE_NORMAL_ON;
517            }
518        } else {
519            if (mSticky) {
520                if (pressed) {
521                    states = KEY_STATE_PRESSED_OFF;
522                } else {
523                    states = KEY_STATE_NORMAL_OFF;
524                }
525            } else {
526                if (pressed) {
527                    states = KEY_STATE_PRESSED;
528                }
529            }
530        }
531        return states;
532    }
533}
534