Key.java revision 9f01ed51d78d9a236d3c321a00ab74165a34630a
1/*
2 * Copyright (C) 2010 Google Inc.
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 com.android.inputmethod.keyboard.KeyboardParser.ParseException;
20import com.android.inputmethod.keyboard.KeyStyles.KeyStyle;
21import com.android.inputmethod.latin.R;
22
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.content.res.XmlResourceParser;
26import android.graphics.drawable.Drawable;
27import android.text.TextUtils;
28import android.util.Xml;
29
30/**
31 * Class for describing the position and characteristics of a single key in the keyboard.
32 */
33public class Key {
34    /**
35     * All the key codes (unicode or custom code) that this key could generate, zero'th
36     * being the most important.
37     */
38    public final int[] mCodes;
39    /** The unicode that this key generates in manual temporary upper case mode. */
40    public final int mManualTemporaryUpperCaseCode;
41
42    /** Label to display */
43    public final CharSequence mLabel;
44    /** Option of the label */
45    public final int mLabelOption;
46
47    /** Icon to display instead of a label. Icon takes precedence over a label */
48    private Drawable mIcon;
49    /** Preview version of the icon, for the preview popup */
50    private Drawable mPreviewIcon;
51    /** Hint icon to display on the key in conjunction with the label */
52    public final Drawable mHintIcon;
53    /**
54     * The hint icon to display on the key when keyboard is in manual temporary upper case
55     * mode.
56     */
57    public final Drawable mManualTemporaryUpperCaseHintIcon;
58
59    /** Width of the key, not including the gap */
60    public final int mWidth;
61    /** Height of the key, not including the gap */
62    public final int mHeight;
63    /** The horizontal gap before this key */
64    public final int mGap;
65    /** Whether this key is sticky, i.e., a toggle key */
66    public final boolean mSticky;
67    /** X coordinate of the key in the keyboard layout */
68    public final int mX;
69    /** Y coordinate of the key in the keyboard layout */
70    public final int mY;
71    /** Text to output when pressed. This can be multiple characters, like ".com" */
72    public final CharSequence mOutputText;
73    /** Popup characters */
74    public final CharSequence mPopupCharacters;
75    /**
76     * If this key pops up a mini keyboard, this is the resource id for the XML layout for that
77     * keyboard.
78     */
79    public final int mPopupResId;
80
81    /**
82     * Flags that specify the anchoring to edges of the keyboard for detecting touch events
83     * that are just out of the boundary of the key. This is a bit mask of
84     * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT},
85     * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}.
86     */
87    public final int mEdgeFlags;
88    /** Whether this is a modifier key, such as Shift or Alt */
89    public final boolean mModifier;
90    /** Whether this key repeats itself when held down */
91    public final boolean mRepeatable;
92
93    /** The Keyboard that this key belongs to */
94    private final Keyboard mKeyboard;
95
96    /** The current pressed state of this key */
97    public boolean mPressed;
98    /** If this is a sticky key, is it on? */
99    public boolean mOn;
100
101    private final static int[] KEY_STATE_NORMAL_ON = {
102        android.R.attr.state_checkable,
103        android.R.attr.state_checked
104    };
105
106    private final static int[] KEY_STATE_PRESSED_ON = {
107        android.R.attr.state_pressed,
108        android.R.attr.state_checkable,
109        android.R.attr.state_checked
110    };
111
112    private final static int[] KEY_STATE_NORMAL_OFF = {
113        android.R.attr.state_checkable
114    };
115
116    private final static int[] KEY_STATE_PRESSED_OFF = {
117        android.R.attr.state_pressed,
118        android.R.attr.state_checkable
119    };
120
121    private final static int[] KEY_STATE_NORMAL = {
122    };
123
124    private final static int[] KEY_STATE_PRESSED = {
125        android.R.attr.state_pressed
126    };
127
128    // functional normal state (with properties)
129    private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = {
130            android.R.attr.state_single
131    };
132
133    // functional pressed state (with properties)
134    private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = {
135            android.R.attr.state_single,
136            android.R.attr.state_pressed
137    };
138
139    /** Create an empty key with no attributes. */
140    public Key(Row row, char letter, int x, int y) {
141        mKeyboard = row.getKeyboard();
142        mHeight = row.mDefaultHeight - row.mVerticalGap;
143        mGap = row.mDefaultHorizontalGap;
144        mWidth = row.mDefaultWidth - mGap;
145        mEdgeFlags = row.mRowEdgeFlags;
146        mHintIcon = null;
147        mManualTemporaryUpperCaseHintIcon = null;
148        mManualTemporaryUpperCaseCode = 0;
149        mLabelOption = 0;
150        mModifier = false;
151        mSticky = false;
152        mRepeatable = false;
153        mOutputText = null;
154        mPopupCharacters = null;
155        mPopupResId = 0;
156        mLabel = String.valueOf(letter);
157        mCodes = new int[] { letter };
158        // Horizontal gap is divided equally to both sides of the key.
159        mX = x + mGap / 2;
160        mY = y;
161    }
162
163    /** Create a key with the given top-left coordinate and extract its attributes from
164     * the XML parser.
165     * @param res resources associated with the caller's context
166     * @param row the row that this key belongs to. The row must already be attached to
167     * a {@link Keyboard}.
168     * @param x the x coordinate of the top-left
169     * @param y the y coordinate of the top-left
170     * @param parser the XML parser containing the attributes for this key
171     */
172    public Key(Resources res, Row row, int x, int y, XmlResourceParser parser,
173            KeyStyles keyStyles) {
174        mKeyboard = row.getKeyboard();
175
176        TypedArray a = res.obtainAttributes(Xml.asAttributeSet(parser),
177                R.styleable.Keyboard);
178        mHeight = KeyboardParser.getDimensionOrFraction(a,
179                R.styleable.Keyboard_rowHeight,
180                mKeyboard.getKeyboardHeight(), row.mDefaultHeight) - row.mVerticalGap;
181        mGap = KeyboardParser.getDimensionOrFraction(a,
182                R.styleable.Keyboard_horizontalGap,
183                mKeyboard.getDisplayWidth(), row.mDefaultHorizontalGap);
184        mWidth = KeyboardParser.getDimensionOrFraction(a,
185                R.styleable.Keyboard_keyWidth,
186                mKeyboard.getDisplayWidth(), row.mDefaultWidth) - mGap;
187        a.recycle();
188
189        a = res.obtainAttributes(Xml.asAttributeSet(parser), R.styleable.Keyboard_Key);
190
191        final KeyStyle style;
192        if (a.hasValue(R.styleable.Keyboard_Key_keyStyle)) {
193            String styleName = a.getString(R.styleable.Keyboard_Key_keyStyle);
194            style = keyStyles.getKeyStyle(styleName);
195            if (style == null)
196                throw new ParseException("Unknown key style: " + styleName, parser);
197        } else {
198            style = keyStyles.getEmptyKeyStyle();
199        }
200
201        // Horizontal gap is divided equally to both sides of the key.
202        this.mX = x + mGap / 2;
203        this.mY = y;
204
205        mPreviewIcon = style.getDrawable(a, R.styleable.Keyboard_Key_iconPreview);
206        Keyboard.setDefaultBounds(mPreviewIcon);
207        final CharSequence popupCharacters = style.getText(a,
208                R.styleable.Keyboard_Key_popupCharacters);
209        final int popupResId = style.getResourceId(a, R.styleable.Keyboard_Key_popupKeyboard, 0);
210        // Set popup keyboard resource and characters only when both are specified.
211        if (popupResId != 0 && !TextUtils.isEmpty(popupCharacters)) {
212            mPopupResId = popupResId;
213            mPopupCharacters = popupCharacters;
214        } else {
215            mPopupResId = 0;
216            mPopupCharacters = null;
217        }
218        mRepeatable = style.getBoolean(a, R.styleable.Keyboard_Key_isRepeatable, false);
219        mModifier = style.getBoolean(a, R.styleable.Keyboard_Key_isModifier, false);
220        mSticky = style.getBoolean(a, R.styleable.Keyboard_Key_isSticky, false);
221        mEdgeFlags = style.getFlag(a, R.styleable.Keyboard_Key_keyEdgeFlags, 0)
222                | row.mRowEdgeFlags;
223
224        mIcon = style.getDrawable(a, R.styleable.Keyboard_Key_keyIcon);
225        Keyboard.setDefaultBounds(mIcon);
226        mHintIcon = style.getDrawable(a, R.styleable.Keyboard_Key_keyHintIcon);
227        Keyboard.setDefaultBounds(mHintIcon);
228        mManualTemporaryUpperCaseHintIcon = style.getDrawable(a,
229                R.styleable.Keyboard_Key_manualTemporaryUpperCaseHintIcon);
230        Keyboard.setDefaultBounds(mManualTemporaryUpperCaseHintIcon);
231
232        mLabel = style.getText(a, R.styleable.Keyboard_Key_keyLabel);
233        mLabelOption = style.getFlag(a, R.styleable.Keyboard_Key_keyLabelOption, 0);
234        mManualTemporaryUpperCaseCode = style.getInt(a,
235                R.styleable.Keyboard_Key_manualTemporaryUpperCaseCode, 0);
236        mOutputText = style.getText(a, R.styleable.Keyboard_Key_keyOutputText);
237        // Choose the first letter of the label as primary code if not specified.
238        final int[] codes = style.getIntArray(a, R.styleable.Keyboard_Key_codes);
239        if (codes == null && !TextUtils.isEmpty(mLabel)) {
240            mCodes = new int[] { mLabel.charAt(0) };
241        } else {
242            mCodes = codes;
243        }
244
245        final Drawable shiftedIcon = style.getDrawable(a,
246                R.styleable.Keyboard_Key_shiftedIcon);
247        if (shiftedIcon != null)
248            mKeyboard.getShiftedIcons().put(this, shiftedIcon);
249
250        a.recycle();
251    }
252
253    public Drawable getIcon() {
254        return mIcon;
255    }
256
257    public Drawable getPreviewIcon() {
258        return mPreviewIcon;
259    }
260
261    public void setIcon(Drawable icon) {
262        mIcon = icon;
263    }
264
265    public void setPreviewIcon(Drawable icon) {
266        mPreviewIcon = icon;
267    }
268
269    /**
270     * Informs the key that it has been pressed, in case it needs to change its appearance or
271     * state.
272     * @see #onReleased(boolean)
273     */
274    public void onPressed() {
275        mPressed = !mPressed;
276    }
277
278    /**
279     * Changes the pressed state of the key. If it is a sticky key, it will also change the
280     * toggled state of the key if the finger was release inside.
281     * @param inside whether the finger was released inside the key
282     * @see #onPressed()
283     */
284    public void onReleased(boolean inside) {
285        mPressed = !mPressed;
286        if (mSticky && !mKeyboard.isShiftLockEnabled(this))
287            mOn = !mOn;
288    }
289
290    public boolean isInside(int x, int y) {
291        return mKeyboard.isInside(this, x, y);
292    }
293
294    /**
295     * Detects if a point falls on this key.
296     * @param x the x-coordinate of the point
297     * @param y the y-coordinate of the point
298     * @return whether or not the point falls on the key. If the key is attached to an edge, it will
299     * assume that all points between the key and the edge are considered to be on the key.
300     */
301    public boolean isOnKey(int x, int y) {
302        final int flags = mEdgeFlags;
303        final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0;
304        final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0;
305        final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0;
306        final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0;
307        final int left = this.mX;
308        final int right = left + this.mWidth;
309        final int top = this.mY;
310        final int bottom = top + this.mHeight;
311        return (x >= left || leftEdge) && (x < right || rightEdge)
312                && (y >= top || topEdge) && (y < bottom || bottomEdge);
313    }
314
315    /**
316     * Returns the square of the distance to the nearest edge of the key and the given point.
317     * @param x the x-coordinate of the point
318     * @param y the y-coordinate of the point
319     * @return the square of the distance of the point from the nearest edge of the key
320     */
321    public int squaredDistanceToEdge(int x, int y) {
322        final int left = this.mX;
323        final int right = left + this.mWidth;
324        final int top = this.mY;
325        final int bottom = top + this.mHeight;
326        final int edgeX = x < left ? left : (x > right ? right : x);
327        final int edgeY = y < top ? top : (y > bottom ? bottom : y);
328        final int dx = x - edgeX;
329        final int dy = y - edgeY;
330        return dx * dx + dy * dy;
331    }
332
333    // sticky is used for shift key.  If a key is not sticky and is modifier,
334    // the key will be treated as functional.
335    private boolean isFunctionalKey() {
336        return !mSticky && mModifier;
337    }
338
339    /**
340     * Returns the drawable state for the key, based on the current state and type of the key.
341     * @return the drawable state of the key.
342     * @see android.graphics.drawable.StateListDrawable#setState(int[])
343     */
344    public int[] getCurrentDrawableState() {
345        if (isFunctionalKey()) {
346            if (mPressed) {
347                return KEY_STATE_FUNCTIONAL_PRESSED;
348            } else {
349                return KEY_STATE_FUNCTIONAL_NORMAL;
350            }
351        }
352
353        int[] states = KEY_STATE_NORMAL;
354
355        if (mOn) {
356            if (mPressed) {
357                states = KEY_STATE_PRESSED_ON;
358            } else {
359                states = KEY_STATE_NORMAL_ON;
360            }
361        } else {
362            if (mSticky) {
363                if (mPressed) {
364                    states = KEY_STATE_PRESSED_OFF;
365                } else {
366                    states = KEY_STATE_NORMAL_OFF;
367                }
368            } else {
369                if (mPressed) {
370                    states = KEY_STATE_PRESSED;
371                }
372            }
373        }
374        return states;
375    }
376}
377