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