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