Key.java revision 287f4f83e9faa51efce6cc750c5a26b9556db728
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.KeyboardBuilder;
30import com.android.inputmethod.keyboard.internal.KeyboardBuilder.ParseException;
31import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
32import com.android.inputmethod.keyboard.internal.KeyboardParams;
33import com.android.inputmethod.keyboard.internal.MoreKeySpecParser;
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    private final int mLabelOption;
54    private static final int LABEL_OPTION_ALIGN_LEFT = 0x01;
55    private static final int LABEL_OPTION_ALIGN_RIGHT = 0x02;
56    private 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    private static final int LABEL_OPTION_WITH_ICON_LEFT = 0x1000;
66    private static final int LABEL_OPTION_WITH_ICON_RIGHT = 0x2000;
67    private static final int LABEL_OPTION_AUTO_X_SCALE = 0x4000;
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    /** X coordinate of the key in the keyboard layout */
86    public final int mX;
87    /** Y coordinate of the key in the keyboard layout */
88    public final int mY;
89    /** Text to output when pressed. This can be multiple characters, like ".com" */
90    public final CharSequence mOutputText;
91    /** More keys */
92    public final CharSequence[] mMoreKeys;
93    /** More keys maximum column number */
94    public final int mMaxMoreKeysColumn;
95
96    /**
97     * Flags that specify the anchoring to edges of the keyboard for detecting touch events
98     * that are just out of the boundary of the key. This is a bit mask of
99     * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
100     * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
101     */
102    private int mEdgeFlags;
103
104    /** Background type that represents different key background visual than normal one. */
105    public final int mBackgroundType;
106    public static final int BACKGROUND_TYPE_NORMAL = 0;
107    public static final int BACKGROUND_TYPE_FUNCTIONAL = 1;
108    public static final int BACKGROUND_TYPE_ACTION = 2;
109    public static final int BACKGROUND_TYPE_STICKY = 3;
110
111    /** Whether this key repeats itself when held down */
112    public final boolean mRepeatable;
113
114    /** The current pressed state of this key */
115    private boolean mPressed;
116    /** If this is a sticky key, is its highlight on? */
117    private boolean mHighlightOn;
118    /** Key is enabled and responds on press */
119    private boolean mEnabled = true;
120    /** Whether this key needs to show the "..." popup hint for special purposes */
121    private boolean mNeedsSpecialPopupHint;
122
123    // RTL parenthesis character swapping map.
124    private static final Map<Integer, Integer> sRtlParenthesisMap = new HashMap<Integer, Integer>();
125
126    static {
127        // The all letters need to be mirrored are found at
128        // http://www.unicode.org/Public/6.0.0/ucd/extracted/DerivedBinaryProperties.txt
129        addRtlParenthesisPair('(', ')');
130        addRtlParenthesisPair('[', ']');
131        addRtlParenthesisPair('{', '}');
132        addRtlParenthesisPair('<', '>');
133        // \u00ab: LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
134        // \u00bb: RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
135        addRtlParenthesisPair('\u00ab', '\u00bb');
136        // \u2039: SINGLE LEFT-POINTING ANGLE QUOTATION MARK
137        // \u203a: SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
138        addRtlParenthesisPair('\u2039', '\u203a');
139        // \u2264: LESS-THAN OR EQUAL TO
140        // \u2265: GREATER-THAN OR EQUAL TO
141        addRtlParenthesisPair('\u2264', '\u2265');
142    }
143
144    private static void addRtlParenthesisPair(int left, int right) {
145        sRtlParenthesisMap.put(left, right);
146        sRtlParenthesisMap.put(right, left);
147    }
148
149    public static int getRtlParenthesisCode(int code, boolean isRtl) {
150        if (isRtl && sRtlParenthesisMap.containsKey(code)) {
151            return sRtlParenthesisMap.get(code);
152        } else {
153            return code;
154        }
155    }
156
157    private static int getCode(Resources res, KeyboardParams params, String moreKeySpec) {
158        return getRtlParenthesisCode(
159                MoreKeySpecParser.getCode(res, moreKeySpec), params.mIsRtlKeyboard);
160    }
161
162    private static Drawable getIcon(KeyboardParams params, String moreKeySpec) {
163        return params.mIconsSet.getIcon(MoreKeySpecParser.getIconId(moreKeySpec));
164    }
165
166    /**
167     * This constructor is being used only for key in more keys keyboard.
168     */
169    public Key(Resources res, KeyboardParams params, String moreKeySpec,
170            int x, int y, int width, int height, int edgeFlags) {
171        this(params, MoreKeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec),
172                getCode(res, params, moreKeySpec), MoreKeySpecParser.getOutputText(moreKeySpec),
173                x, y, width, height, edgeFlags);
174    }
175
176    /**
177     * This constructor is being used only for key in popup suggestions pane.
178     */
179    public Key(KeyboardParams params, CharSequence label, CharSequence hintLabel, Drawable icon,
180            int code, CharSequence outputText, int x, int y, int width, int height, int edgeFlags) {
181        mHeight = height - params.mVerticalGap;
182        mHorizontalGap = params.mHorizontalGap;
183        mVerticalGap = params.mVerticalGap;
184        mVisualInsetsLeft = mVisualInsetsRight = 0;
185        mWidth = width - mHorizontalGap;
186        mEdgeFlags = edgeFlags;
187        mHintLabel = hintLabel;
188        mLabelOption = 0;
189        mBackgroundType = BACKGROUND_TYPE_NORMAL;
190        mRepeatable = false;
191        mMoreKeys = null;
192        mMaxMoreKeysColumn = 0;
193        mLabel = label;
194        mOutputText = outputText;
195        mCode = code;
196        mIcon = icon;
197        // Horizontal gap is divided equally to both sides of the key.
198        mX = x + mHorizontalGap / 2;
199        mY = y;
200    }
201
202    /**
203     * Create a key with the given top-left coordinate and extract its attributes from the XML
204     * parser.
205     * @param res resources associated with the caller's context
206     * @param params the keyboard building parameters.
207     * @param row the row that this key belongs to. row's x-coordinate will be the right edge of
208     *        this key.
209     * @param parser the XML parser containing the attributes for this key
210     * @param keyStyles active key styles set
211     */
212    public Key(Resources res, KeyboardParams params, KeyboardBuilder.Row row,
213            XmlResourceParser parser, KeyStyles keyStyles) {
214        final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
215        mVerticalGap = params.mVerticalGap;
216        mHeight = row.mRowHeight - mVerticalGap;
217
218        final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
219                R.styleable.Keyboard_Key);
220
221        final KeyStyle style;
222        if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
223            String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle);
224            style = keyStyles.getKeyStyle(styleName);
225            if (style == null)
226                throw new ParseException("Unknown key style: " + styleName, parser);
227        } else {
228            style = keyStyles.getEmptyKeyStyle();
229        }
230
231        final float keyXPos = row.getKeyX(keyAttr);
232        final float keyWidth = row.getKeyWidth(keyAttr, keyXPos);
233
234        // Horizontal gap is divided equally to both sides of the key.
235        mX = (int) (keyXPos + horizontalGap / 2);
236        mY = row.getKeyY();
237        mWidth = (int) (keyWidth - horizontalGap);
238        mHorizontalGap = (int) horizontalGap;
239        // Update row to have current x coordinate.
240        row.setXPos(keyXPos + keyWidth);
241
242        final CharSequence[] moreKeys = style.getTextArray(keyAttr,
243                R.styleable.Keyboard_Key_moreKeys);
244        // In Arabic symbol layouts, we'd like to keep digits in more keys regardless of
245        // config_digit_more_keys_enabled.
246        if (params.mId.isAlphabetKeyboard()
247                && !res.getBoolean(R.bool.config_digit_more_keys_enabled)) {
248            mMoreKeys = MoreKeySpecParser.filterOut(res, moreKeys, MoreKeySpecParser.DIGIT_FILTER);
249        } else {
250            mMoreKeys = moreKeys;
251        }
252        mMaxMoreKeysColumn = style.getInt(keyAttr,
253                R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMiniKeyboardColumn);
254
255        mBackgroundType = style.getInt(keyAttr,
256                R.styleable.Keyboard_Key_backgroundType, BACKGROUND_TYPE_NORMAL);
257        mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false);
258        mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true);
259        mEdgeFlags = 0;
260
261        final KeyboardIconsSet iconsSet = params.mIconsSet;
262        mVisualInsetsLeft = (int) KeyboardBuilder.getDimensionOrFraction(keyAttr,
263                R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0);
264        mVisualInsetsRight = (int) KeyboardBuilder.getDimensionOrFraction(keyAttr,
265                R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0);
266        mPreviewIcon = iconsSet.getIcon(style.getInt(keyAttr,
267                R.styleable.Keyboard_Key_keyIconPreview, KeyboardIconsSet.ICON_UNDEFINED));
268        mIcon = iconsSet.getIcon(style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIcon,
269                KeyboardIconsSet.ICON_UNDEFINED));
270        final int shiftedIconId = style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted,
271                KeyboardIconsSet.ICON_UNDEFINED);
272        if (shiftedIconId != KeyboardIconsSet.ICON_UNDEFINED) {
273            final Drawable shiftedIcon = iconsSet.getIcon(shiftedIconId);
274            params.addShiftedIcon(this, shiftedIcon);
275        }
276        mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
277
278        mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
279        mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0);
280        mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText);
281        // Choose the first letter of the label as primary code if not
282        // specified.
283        final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code,
284                Keyboard.CODE_UNSPECIFIED);
285        if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) {
286            final int firstChar = mLabel.charAt(0);
287            mCode = getRtlParenthesisCode(firstChar, params.mIsRtlKeyboard);
288        } else if (code != Keyboard.CODE_UNSPECIFIED) {
289            mCode = code;
290        } else {
291            mCode = Keyboard.CODE_DUMMY;
292        }
293
294        keyAttr.recycle();
295    }
296
297    public void addEdgeFlags(int flags) {
298        mEdgeFlags |= flags;
299    }
300
301    public boolean isSticky() {
302        return mBackgroundType == BACKGROUND_TYPE_STICKY;
303    }
304
305    public boolean isSpacer() {
306        return false;
307    }
308
309    public Typeface selectTypeface(Typeface defaultTypeface) {
310        // TODO: Handle "bold" here too?
311        if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) {
312            return Typeface.DEFAULT;
313        } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) {
314            return Typeface.MONOSPACE;
315        } else {
316            return defaultTypeface;
317        }
318    }
319
320    public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) {
321        if (mLabel.length() > 1
322                && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO
323                        | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) {
324            return label;
325        } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) {
326            return hintLabel;
327        } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) {
328            return largeLetter;
329        } else {
330            return letter;
331        }
332    }
333
334    public boolean isAlignLeft() {
335        return (mLabelOption & LABEL_OPTION_ALIGN_LEFT) != 0;
336    }
337
338    public boolean isAlignRight() {
339        return (mLabelOption & LABEL_OPTION_ALIGN_RIGHT) != 0;
340    }
341
342    public boolean isAlignLeftOfCenter() {
343        return (mLabelOption & LABEL_OPTION_ALIGN_LEFT_OF_CENTER) != 0;
344    }
345
346    public boolean hasPopupHint() {
347        return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0;
348    }
349
350    public void setNeedsSpecialPopupHint(boolean needsSpecialPopupHint) {
351        mNeedsSpecialPopupHint = needsSpecialPopupHint;
352    }
353
354    public boolean needsSpecialPopupHint() {
355        return mNeedsSpecialPopupHint;
356    }
357
358    public boolean hasUppercaseLetter() {
359        return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0;
360    }
361
362    public boolean hasHintLabel() {
363        return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0;
364    }
365
366    public boolean hasLabelWithIconLeft() {
367        return (mLabelOption & LABEL_OPTION_WITH_ICON_LEFT) != 0;
368    }
369
370    public boolean hasLabelWithIconRight() {
371        return (mLabelOption & LABEL_OPTION_WITH_ICON_RIGHT) != 0;
372    }
373
374    public boolean needsXScale() {
375        return (mLabelOption & LABEL_OPTION_AUTO_X_SCALE) != 0;
376    }
377
378    public Drawable getIcon() {
379        return mIcon;
380    }
381
382    public Drawable getPreviewIcon() {
383        return mPreviewIcon;
384    }
385
386    public void setIcon(Drawable icon) {
387        mIcon = icon;
388    }
389
390    public void setPreviewIcon(Drawable icon) {
391        mPreviewIcon = icon;
392    }
393
394    /**
395     * Informs the key that it has been pressed, in case it needs to change its appearance or
396     * state.
397     * @see #onReleased()
398     */
399    public void onPressed() {
400        mPressed = true;
401    }
402
403    /**
404     * Informs the key that it has been released, in case it needs to change its appearance or
405     * state.
406     * @see #onPressed()
407     */
408    public void onReleased() {
409        mPressed = false;
410    }
411
412    public void setHighlightOn(boolean highlightOn) {
413        mHighlightOn = highlightOn;
414    }
415
416    public boolean isEnabled() {
417        return mEnabled;
418    }
419
420    public void setEnabled(boolean enabled) {
421        mEnabled = enabled;
422    }
423
424    /**
425     * Detects if a point falls on this key.
426     * @param x the x-coordinate of the point
427     * @param y the y-coordinate of the point
428     * @return whether or not the point falls on the key. If the key is attached to an edge, it will
429     * assume that all points between the key and the edge are considered to be on the key.
430     */
431    public boolean isOnKey(int x, int y) {
432        final int left = mX - mHorizontalGap / 2;
433        final int right = left + mWidth + mHorizontalGap;
434        final int top = mY;
435        final int bottom = top + mHeight + mVerticalGap;
436        final int flags = mEdgeFlags;
437        if (flags == 0) {
438            return x >= left && x <= right && y >= top && y <= bottom;
439        }
440        final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
441        final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
442        final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
443        final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
444        // In order to mitigate rounding errors, we use (left <= x <= right) here.
445        return (x >= left || leftEdge) && (x <= right || rightEdge)
446                && (y >= top || topEdge) && (y <= bottom || bottomEdge);
447    }
448
449    /**
450     * Returns the square of the distance to the nearest edge of the key and the given point.
451     * @param x the x-coordinate of the point
452     * @param y the y-coordinate of the point
453     * @return the square of the distance of the point from the nearest edge of the key
454     */
455    public int squaredDistanceToEdge(int x, int y) {
456        final int left = mX;
457        final int right = left + mWidth;
458        final int top = mY;
459        final int bottom = top + mHeight;
460        final int edgeX = x < left ? left : (x > right ? right : x);
461        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
462        final int dx = x - edgeX;
463        final int dy = y - edgeY;
464        return dx * dx + dy * dy;
465    }
466
467    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_ON = {
468        android.R.attr.state_checkable,
469        android.R.attr.state_checked
470    };
471
472    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_ON = {
473        android.R.attr.state_pressed,
474        android.R.attr.state_checkable,
475        android.R.attr.state_checked
476    };
477
478    private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_OFF = {
479        android.R.attr.state_checkable
480    };
481
482    private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_OFF = {
483        android.R.attr.state_pressed,
484        android.R.attr.state_checkable
485    };
486
487    private final static int[] KEY_STATE_NORMAL = {
488    };
489
490    private final static int[] KEY_STATE_PRESSED = {
491        android.R.attr.state_pressed
492    };
493
494    // functional normal state (with properties)
495    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
496            android.R.attr.state_single
497    };
498
499    // functional pressed state (with properties)
500    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
501            android.R.attr.state_single,
502            android.R.attr.state_pressed
503    };
504
505    // action normal state (with properties)
506    private static final int[] KEY_STATE_ACTIVE_NORMAL = {
507            android.R.attr.state_active
508    };
509
510    // action pressed state (with properties)
511    private static final int[] KEY_STATE_ACTIVE_PRESSED = {
512            android.R.attr.state_active,
513            android.R.attr.state_pressed
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
524        switch (mBackgroundType) {
525        case BACKGROUND_TYPE_FUNCTIONAL:
526            return pressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL;
527        case BACKGROUND_TYPE_ACTION:
528            return pressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL;
529        case BACKGROUND_TYPE_STICKY:
530            if (mHighlightOn) {
531                return pressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON;
532            } else {
533                return pressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF;
534            }
535        default: /* BACKGROUND_TYPE_NORMAL */
536            return pressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL;
537        }
538    }
539
540    public static class Spacer extends Key {
541        public Spacer(Resources res, KeyboardParams params, KeyboardBuilder.Row row,
542                XmlResourceParser parser, KeyStyles keyStyles) {
543            super(res, params, row, parser, keyStyles);
544        }
545
546        /**
547         * This constructor is being used only for divider in more keys keyboard.
548         */
549        public Spacer(KeyboardParams params, Drawable icon, int x, int y, int width, int height) {
550            super(params, null, null, icon, Keyboard.CODE_DUMMY, null, x, y, width, height, 0);
551        }
552
553        @Override
554        public boolean isSpacer() {
555            return true;
556        }
557    }
558}
559