Keyboard.java revision 391a7ce6d8d20825c13764c3730f8b4dd1053b31
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.latin.R;
20
21import org.xmlpull.v1.XmlPullParserException;
22
23import android.content.Context;
24import android.content.res.Resources;
25import android.content.res.XmlResourceParser;
26import android.graphics.drawable.Drawable;
27import android.util.Log;
28
29import java.io.IOException;
30import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.HashSet;
33import java.util.List;
34import java.util.Map;
35
36/**
37 * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard
38 * consists of rows of keys.
39 * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
40 * <pre>
41 * &lt;Keyboard
42 *         latin:keyWidth="%10p"
43 *         latin:keyHeight="50px"
44 *         latin:horizontalGap="2px"
45 *         latin:verticalGap="2px" &gt;
46 *     &lt;Row latin:keyWidth="32px" &gt;
47 *         &lt;Key latin:keyLabel="A" /&gt;
48 *         ...
49 *     &lt;/Row&gt;
50 *     ...
51 * &lt;/Keyboard&gt;
52 * </pre>
53 */
54public class Keyboard {
55    private static final String TAG = "Keyboard";
56
57    public static final int EDGE_LEFT = 0x01;
58    public static final int EDGE_RIGHT = 0x02;
59    public static final int EDGE_TOP = 0x04;
60    public static final int EDGE_BOTTOM = 0x08;
61
62    public static final int CODE_ENTER = '\n';
63    public static final int CODE_TAB = '\t';
64    public static final int CODE_SPACE = ' ';
65    public static final int CODE_PERIOD = '.';
66
67    public static final int CODE_SHIFT = -1;
68    public static final int CODE_MODE_CHANGE = -2;
69    public static final int CODE_CANCEL = -3;
70    public static final int CODE_DONE = -4;
71    public static final int CODE_DELETE = -5;
72    public static final int CODE_ALT = -6;
73
74    public static final int CODE_OPTIONS = -100;
75    public static final int CODE_OPTIONS_LONGPRESS = -101;
76    public static final int CODE_CAPSLOCK = -103;
77    public static final int CODE_NEXT_LANGUAGE = -104;
78    public static final int CODE_PREV_LANGUAGE = -105;
79    // TODO: remove this once LatinIME stops referring to this.
80    public static final int CODE_VOICE = -109;
81
82    /** Horizontal gap default for all rows */
83    int mDefaultHorizontalGap;
84
85    /** Default key width */
86    int mDefaultWidth;
87
88    /** Default key height */
89    int mDefaultHeight;
90
91    /** Default gap between rows */
92    int mDefaultVerticalGap;
93
94    /** List of shift keys in this keyboard and its icons and state */
95    private final List<Key> mShiftKeys = new ArrayList<Key>();
96    private final HashMap<Key, Drawable> mShiftedIcons = new HashMap<Key, Drawable>();
97    private final HashMap<Key, Drawable> mNormalShiftIcons = new HashMap<Key, Drawable>();
98    private final HashSet<Key> mShiftLockEnabled = new HashSet<Key>();
99    private final KeyboardShiftState mShiftState = new KeyboardShiftState();
100
101    /** Space key and its icons */
102    protected Key mSpaceKey;
103    protected Drawable mSpaceIcon;
104    protected Drawable mSpacePreviewIcon;
105
106    /** Total height of the keyboard, including the padding and keys */
107    private int mTotalHeight;
108
109    /**
110     * Total width of the keyboard, including left side gaps and keys, but not any gaps on the
111     * right side.
112     */
113    private int mTotalWidth;
114
115    /** List of keys in this keyboard */
116    private final List<Key> mKeys = new ArrayList<Key>();
117
118    /** Width of the screen available to fit the keyboard */
119    final int mDisplayWidth;
120
121    /** Height of the screen */
122    final int mDisplayHeight;
123
124    protected final KeyboardId mId;
125
126    // Variables for pre-computing nearest keys.
127
128    public final int GRID_WIDTH;
129    public final int GRID_HEIGHT;
130    private final int GRID_SIZE;
131    private int mCellWidth;
132    private int mCellHeight;
133    private int[][] mGridNeighbors;
134    private int mProximityThreshold;
135    private static int[] EMPTY_INT_ARRAY = new int[0];
136    /** Number of key widths from current touch point to search for nearest keys. */
137    private static float SEARCH_DISTANCE = 1.2f;
138
139    /**
140     * Creates a keyboard from the given xml key layout file.
141     * @param context the application or service context
142     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
143     */
144    public Keyboard(Context context, int xmlLayoutResId) {
145        this(context, xmlLayoutResId, null);
146    }
147
148    /**
149     * Creates a keyboard from the given keyboard identifier.
150     * @param context the application or service context
151     * @param id keyboard identifier
152     */
153    public Keyboard(Context context, KeyboardId id) {
154        this(context, id.getXmlId(), id);
155    }
156
157    /**
158     * Creates a keyboard from the given xml key layout file.
159     * @param context the application or service context
160     * @param xmlLayoutResId the resource file that contains the keyboard layout and keys.
161     * @param id keyboard identifier
162     */
163    private Keyboard(Context context, int xmlLayoutResId, KeyboardId id) {
164        this(context, xmlLayoutResId, id,
165                context.getResources().getDisplayMetrics().widthPixels,
166                context.getResources().getDisplayMetrics().heightPixels);
167    }
168
169    private Keyboard(Context context, int xmlLayoutResId, KeyboardId id, int width,
170            int height) {
171        Resources res = context.getResources();
172        GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
173        GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
174        GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
175
176        mDisplayWidth = width;
177        mDisplayHeight = height;
178
179        mDefaultHorizontalGap = 0;
180        setKeyWidth(mDisplayWidth / 10);
181        mDefaultVerticalGap = 0;
182        mDefaultHeight = mDefaultWidth;
183        mId = id;
184        loadKeyboard(context, xmlLayoutResId);
185    }
186
187    /**
188     * <p>Creates a blank keyboard from the given resource file and populates it with the specified
189     * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
190     * </p>
191     * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
192     * possible in each row.</p>
193     * @param context the application or service context
194     * @param layoutTemplateResId the layout template file, containing no keys.
195     * @param characters the list of characters to display on the keyboard. One key will be created
196     * for each character.
197     * @param columns the number of columns of keys to display. If this number is greater than the
198     * number of keys that can fit in a row, it will be ignored. If this number is -1, the
199     * keyboard will fit as many keys as possible in each row.
200     */
201    public Keyboard(Context context, int layoutTemplateResId,
202            CharSequence characters, int columns, int horizontalPadding) {
203        this(context, layoutTemplateResId);
204        int x = 0;
205        int y = 0;
206        int column = 0;
207        mTotalWidth = 0;
208
209        Row row = new Row(this);
210        row.mDefaultHeight = mDefaultHeight;
211        row.mDefaultWidth = mDefaultWidth;
212        row.mDefaultHorizontalGap = mDefaultHorizontalGap;
213        row.mVerticalGap = mDefaultVerticalGap;
214        row.mRowEdgeFlags = EDGE_TOP | EDGE_BOTTOM;
215        final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
216        for (int i = 0; i < characters.length(); i++) {
217            char c = characters.charAt(i);
218            if (column >= maxColumns
219                    || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
220                x = 0;
221                y += mDefaultVerticalGap + mDefaultHeight;
222                column = 0;
223            }
224            final Key key = new Key(row);
225            // Horizontal gap is divided equally to both sides of the key.
226            key.mX = x + key.mGap / 2;
227            key.mY = y;
228            key.mLabel = String.valueOf(c);
229            key.mCodes = new int[] { c };
230            column++;
231            x += key.mWidth + key.mGap;
232            mKeys.add(key);
233            if (x > mTotalWidth) {
234                mTotalWidth = x;
235            }
236        }
237        mTotalHeight = y + mDefaultHeight;
238    }
239
240    public KeyboardId getKeyboardId() {
241        return mId;
242    }
243
244    public List<Key> getKeys() {
245        return mKeys;
246    }
247
248    protected int getHorizontalGap() {
249        return mDefaultHorizontalGap;
250    }
251
252    protected void setHorizontalGap(int gap) {
253        mDefaultHorizontalGap = gap;
254    }
255
256    protected int getVerticalGap() {
257        return mDefaultVerticalGap;
258    }
259
260    protected void setVerticalGap(int gap) {
261        mDefaultVerticalGap = gap;
262    }
263
264    protected int getKeyHeight() {
265        return mDefaultHeight;
266    }
267
268    protected void setKeyHeight(int height) {
269        mDefaultHeight = height;
270    }
271
272    protected int getKeyWidth() {
273        return mDefaultWidth;
274    }
275
276    protected void setKeyWidth(int width) {
277        mDefaultWidth = width;
278        final int threshold = (int) (width * SEARCH_DISTANCE);
279        mProximityThreshold = threshold * threshold;
280    }
281
282    /**
283     * Returns the total height of the keyboard
284     * @return the total height of the keyboard
285     */
286    public int getHeight() {
287        return mTotalHeight;
288    }
289
290    public int getMinWidth() {
291        return mTotalWidth;
292    }
293
294    public int getKeyboardHeight() {
295        return mDisplayHeight;
296    }
297
298    public int getKeyboardWidth() {
299        return mDisplayWidth;
300    }
301
302    public List<Key> getShiftKeys() {
303        return mShiftKeys;
304    }
305
306    public Map<Key, Drawable> getShiftedIcons() {
307        return mShiftedIcons;
308    }
309
310    public void enableShiftLock() {
311        for (final Key key : getShiftKeys()) {
312            mShiftLockEnabled.add(key);
313            mNormalShiftIcons.put(key, key.mIcon);
314        }
315    }
316
317    public boolean isShiftLockEnabled(Key key) {
318        return mShiftLockEnabled.contains(key);
319    }
320
321    public boolean setShiftLocked(boolean newShiftLockState) {
322        final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
323        for (final Key key : getShiftKeys()) {
324            key.mOn = newShiftLockState;
325            key.mIcon = newShiftLockState ? shiftedIcons.get(key) : mNormalShiftIcons.get(key);
326        }
327        mShiftState.setShiftLocked(newShiftLockState);
328        return true;
329    }
330
331    public boolean isShiftLocked() {
332        return mShiftState.isShiftLocked();
333    }
334
335    public boolean setShifted(boolean newShiftState) {
336        final Map<Key, Drawable> shiftedIcons = getShiftedIcons();
337        for (final Key key : getShiftKeys()) {
338            if (!newShiftState && !mShiftState.isShiftLocked()) {
339                key.mIcon = mNormalShiftIcons.get(key);
340            } else if (newShiftState && !mShiftState.isShiftedOrShiftLocked()) {
341                key.mIcon = shiftedIcons.get(key);
342            }
343        }
344        return mShiftState.setShifted(newShiftState);
345    }
346
347    public boolean isShiftedOrShiftLocked() {
348        return mShiftState.isShiftedOrShiftLocked();
349    }
350
351    public void setAutomaticTemporaryUpperCase() {
352        setShifted(true);
353        mShiftState.setAutomaticTemporaryUpperCase();
354    }
355
356    public boolean isAutomaticTemporaryUpperCase() {
357        return isAlphaKeyboard() && mShiftState.isAutomaticTemporaryUpperCase();
358    }
359
360    public boolean isManualTemporaryUpperCase() {
361        return isAlphaKeyboard() && mShiftState.isManualTemporaryUpperCase();
362    }
363
364    public KeyboardShiftState getKeyboardShiftState() {
365        return mShiftState;
366    }
367
368    public boolean isAlphaKeyboard() {
369        return mId != null && mId.isAlphabetKeyboard();
370    }
371
372    public boolean isPhoneKeyboard() {
373        return mId != null && mId.isPhoneKeyboard();
374    }
375
376    public boolean isNumberKeyboard() {
377        return mId != null && mId.isNumberKeyboard();
378    }
379
380    public void setSpaceKey(Key space) {
381        mSpaceKey = space;
382        mSpaceIcon = space.mIcon;
383        mSpacePreviewIcon = space.mPreviewIcon;
384    }
385
386    private void computeNearestNeighbors() {
387        // Round-up so we don't have any pixels outside the grid
388        mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
389        mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
390        mGridNeighbors = new int[GRID_SIZE][];
391        final int[] indices = new int[mKeys.size()];
392        final int gridWidth = GRID_WIDTH * mCellWidth;
393        final int gridHeight = GRID_HEIGHT * mCellHeight;
394        final int threshold = mProximityThreshold;
395        for (int x = 0; x < gridWidth; x += mCellWidth) {
396            for (int y = 0; y < gridHeight; y += mCellHeight) {
397                final int centerX = x + mCellWidth / 2;
398                final int centerY = y + mCellHeight / 2;
399                int count = 0;
400                for (int i = 0; i < mKeys.size(); i++) {
401                    final Key key = mKeys.get(i);
402                    if (key.squaredDistanceToEdge(centerX, centerY) < threshold)
403                        indices[count++] = i;
404                }
405                final int[] cell = new int[count];
406                System.arraycopy(indices, 0, cell, 0, count);
407                mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
408            }
409        }
410    }
411
412    public boolean isInside(Key key, int x, int y) {
413        return key.isOnKey(x, y);
414    }
415
416    /**
417     * Returns the indices of the keys that are closest to the given point.
418     * @param x the x-coordinate of the point
419     * @param y the y-coordinate of the point
420     * @return the array of integer indices for the nearest keys to the given point. If the given
421     * point is out of range, then an array of size zero is returned.
422     */
423    public int[] getNearestKeys(int x, int y) {
424        if (mGridNeighbors == null) computeNearestNeighbors();
425        if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
426            int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
427            if (index < GRID_SIZE) {
428                return mGridNeighbors[index];
429            }
430        }
431        return EMPTY_INT_ARRAY;
432    }
433
434    // TODO should be private
435    protected Row createRowFromXml(Resources res, XmlResourceParser parser) {
436        return new Row(res, this, parser);
437    }
438
439    // TODO should be private
440    protected Key createKeyFromXml(Resources res, Row parent, int x, int y,
441            XmlResourceParser parser, KeyStyles keyStyles) {
442        return new Key(res, parent, x, y, parser, keyStyles);
443    }
444
445    private void loadKeyboard(Context context, int xmlLayoutResId) {
446        try {
447            final Resources res = context.getResources();
448            KeyboardParser parser = new KeyboardParser(this, res);
449            parser.parseKeyboard(res.getXml(xmlLayoutResId));
450            // mTotalWidth is the width of this keyboard which is maximum width of row.
451            mTotalWidth = parser.getMaxRowWidth();
452            mTotalHeight = parser.getTotalHeight();
453        } catch (XmlPullParserException e) {
454            Log.w(TAG, "keyboard XML parse error: " + e);
455            throw new IllegalArgumentException(e);
456        } catch (IOException e) {
457            Log.w(TAG, "keyboard XML parse error: " + e);
458            throw new RuntimeException(e);
459        }
460    }
461
462    protected static void setDefaultBounds(Drawable drawable)  {
463        if (drawable != null)
464            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
465                    drawable.getIntrinsicHeight());
466    }
467}
468