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 * <Keyboard 42 * latin:keyWidth="%10p" 43 * latin:keyHeight="50px" 44 * latin:horizontalGap="2px" 45 * latin:verticalGap="2px" > 46 * <Row latin:keyWidth="32px" > 47 * <Key latin:keyLabel="A" /> 48 * ... 49 * </Row> 50 * ... 51 * </Keyboard> 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