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