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