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