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