Key.java revision 6d9bcd5e1317722207116ab6a3ddfcb152005701
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.res.Resources; 20import android.content.res.TypedArray; 21import android.content.res.XmlResourceParser; 22import android.graphics.Typeface; 23import android.graphics.drawable.Drawable; 24import android.text.TextUtils; 25import android.util.Xml; 26 27import com.android.inputmethod.keyboard.internal.KeyStyles; 28import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle; 29import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 30import com.android.inputmethod.keyboard.internal.KeyboardParser; 31import com.android.inputmethod.keyboard.internal.KeyboardParser.ParseException; 32import com.android.inputmethod.keyboard.internal.PopupCharactersParser; 33import com.android.inputmethod.keyboard.internal.Row; 34import com.android.inputmethod.latin.R; 35 36import java.util.ArrayList; 37import java.util.HashMap; 38import java.util.Map; 39 40/** 41 * Class for describing the position and characteristics of a single key in the keyboard. 42 */ 43public class Key { 44 /** 45 * The key code (unicode or custom code) that this key generates. 46 */ 47 public final int mCode; 48 49 /** Label to display */ 50 public final CharSequence mLabel; 51 /** Hint label to display on the key in conjunction with the label */ 52 public final CharSequence mHintLabel; 53 /** Option of the label */ 54 public final int mLabelOption; 55 public static final int LABEL_OPTION_ALIGN_LEFT = 0x01; 56 public static final int LABEL_OPTION_ALIGN_RIGHT = 0x02; 57 public static final int LABEL_OPTION_ALIGN_LEFT_OF_CENTER = 0x08; 58 private static final int LABEL_OPTION_LARGE_LETTER = 0x10; 59 private static final int LABEL_OPTION_FONT_NORMAL = 0x20; 60 private static final int LABEL_OPTION_FONT_MONO_SPACE = 0x40; 61 private static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x80; 62 private static final int LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO = 0x100; 63 private static final int LABEL_OPTION_HAS_POPUP_HINT = 0x200; 64 private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x400; 65 private static final int LABEL_OPTION_HAS_HINT_LABEL = 0x800; 66 67 /** Icon to display instead of a label. Icon takes precedence over a label */ 68 private Drawable mIcon; 69 /** Preview version of the icon, for the preview popup */ 70 private Drawable mPreviewIcon; 71 72 /** Width of the key, not including the gap */ 73 public final int mWidth; 74 /** Height of the key, not including the gap */ 75 public final int mHeight; 76 /** The horizontal gap around this key */ 77 public final int mGap; 78 /** The visual insets */ 79 public final int mVisualInsetsLeft; 80 public final int mVisualInsetsRight; 81 /** Whether this key is sticky, i.e., a toggle key */ 82 public final boolean mSticky; 83 /** X coordinate of the key in the keyboard layout */ 84 public final int mX; 85 /** Y coordinate of the key in the keyboard layout */ 86 public final int mY; 87 /** Text to output when pressed. This can be multiple characters, like ".com" */ 88 public final CharSequence mOutputText; 89 /** Popup characters */ 90 public final CharSequence[] mPopupCharacters; 91 /** Popup keyboard maximum column number */ 92 public final int mMaxPopupColumn; 93 94 /** 95 * Flags that specify the anchoring to edges of the keyboard for detecting touch events 96 * that are just out of the boundary of the key. This is a bit mask of 97 * {@link Keyboard#EDGE_LEFT}, {@link Keyboard#EDGE_RIGHT}, 98 * {@link Keyboard#EDGE_TOP} and {@link Keyboard#EDGE_BOTTOM}. 99 */ 100 private int mEdgeFlags; 101 /** Whether this is a functional key which has different key top than normal key */ 102 public final boolean mFunctional; 103 /** Whether this key repeats itself when held down */ 104 public final boolean mRepeatable; 105 106 /** The Keyboard that this key belongs to */ 107 private final Keyboard mKeyboard; 108 109 /** The current pressed state of this key */ 110 private boolean mPressed; 111 /** If this is a sticky key, is its highlight on? */ 112 private boolean mHighlightOn; 113 /** Key is enabled and responds on press */ 114 private boolean mEnabled = true; 115 116 // keyWidth constants 117 private static final int KEYWIDTH_FILL_RIGHT = 0; 118 private static final int KEYWIDTH_FILL_BOTH = -1; 119 120 private final static int[] KEY_STATE_NORMAL_ON = { 121 android.R.attr.state_checkable, 122 android.R.attr.state_checked 123 }; 124 125 private final static int[] KEY_STATE_PRESSED_ON = { 126 android.R.attr.state_pressed, 127 android.R.attr.state_checkable, 128 android.R.attr.state_checked 129 }; 130 131 private final static int[] KEY_STATE_NORMAL_OFF = { 132 android.R.attr.state_checkable 133 }; 134 135 private final static int[] KEY_STATE_PRESSED_OFF = { 136 android.R.attr.state_pressed, 137 android.R.attr.state_checkable 138 }; 139 140 private final static int[] KEY_STATE_NORMAL = { 141 }; 142 143 private final static int[] KEY_STATE_PRESSED = { 144 android.R.attr.state_pressed 145 }; 146 147 // functional normal state (with properties) 148 private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = { 149 android.R.attr.state_single 150 }; 151 152 // functional pressed state (with properties) 153 private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = { 154 android.R.attr.state_single, 155 android.R.attr.state_pressed 156 }; 157 158 // RTL parenthesis character swapping map. 159 private static final Map<Integer, Integer> sRtlParenthesisMap = new HashMap<Integer, Integer>(); 160 161 static { 162 // The all letters need to be mirrored are found at 163 // http://www.unicode.org/Public/6.0.0/ucd/extracted/DerivedBinaryProperties.txt 164 addRtlParenthesisPair('(', ')'); 165 addRtlParenthesisPair('[', ']'); 166 addRtlParenthesisPair('{', '}'); 167 addRtlParenthesisPair('<', '>'); 168 // \u00ab: LEFT-POINTING DOUBLE ANGLE QUOTATION MARK 169 // \u00bb: RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK 170 addRtlParenthesisPair('\u00ab', '\u00bb'); 171 // \u2039: SINGLE LEFT-POINTING ANGLE QUOTATION MARK 172 // \u203a: SINGLE RIGHT-POINTING ANGLE QUOTATION MARK 173 addRtlParenthesisPair('\u2039', '\u203a'); 174 // \u2264: LESS-THAN OR EQUAL TO 175 // \u2265: GREATER-THAN OR EQUAL TO 176 addRtlParenthesisPair('\u2264', '\u2265'); 177 } 178 179 private static void addRtlParenthesisPair(int left, int right) { 180 sRtlParenthesisMap.put(left, right); 181 sRtlParenthesisMap.put(right, left); 182 } 183 184 public static int getRtlParenthesisCode(int code) { 185 if (sRtlParenthesisMap.containsKey(code)) { 186 return sRtlParenthesisMap.get(code); 187 } else { 188 return code; 189 } 190 } 191 192 /** 193 * This constructor is being used only for key in popup mini keyboard. 194 */ 195 public Key(Resources res, Keyboard keyboard, CharSequence popupCharacter, int x, int y, 196 int width, int height, int edgeFlags) { 197 mKeyboard = keyboard; 198 mHeight = height - keyboard.getVerticalGap(); 199 mGap = keyboard.getHorizontalGap(); 200 mVisualInsetsLeft = mVisualInsetsRight = 0; 201 mWidth = width - mGap; 202 mEdgeFlags = edgeFlags; 203 mHintLabel = null; 204 mLabelOption = 0; 205 mFunctional = false; 206 mSticky = false; 207 mRepeatable = false; 208 mPopupCharacters = null; 209 mMaxPopupColumn = 0; 210 final String popupSpecification = popupCharacter.toString(); 211 mLabel = PopupCharactersParser.getLabel(popupSpecification); 212 mOutputText = PopupCharactersParser.getOutputText(popupSpecification); 213 final int code = PopupCharactersParser.getCode(res, popupSpecification); 214 mCode = keyboard.isRtlKeyboard() ? getRtlParenthesisCode(code) : code; 215 mIcon = keyboard.mIconsSet.getIcon(PopupCharactersParser.getIconId(popupSpecification)); 216 // Horizontal gap is divided equally to both sides of the key. 217 mX = x + mGap / 2; 218 mY = y; 219 } 220 221 /** 222 * Create a key with the given top-left coordinate and extract its attributes from the XML 223 * parser. 224 * @param res resources associated with the caller's context 225 * @param row the row that this key belongs to. The row must already be attached to 226 * a {@link Keyboard}. 227 * @param x the x coordinate of the top-left 228 * @param y the y coordinate of the top-left 229 * @param parser the XML parser containing the attributes for this key 230 * @param keyStyles active key styles set 231 */ 232 public Key(Resources res, Row row, int x, int y, XmlResourceParser parser, 233 KeyStyles keyStyles) { 234 mKeyboard = row.getKeyboard(); 235 236 final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 237 R.styleable.Keyboard); 238 int keyWidth; 239 try { 240 mHeight = KeyboardParser.getDimensionOrFraction(keyboardAttr, 241 R.styleable.Keyboard_rowHeight, 242 mKeyboard.getKeyboardHeight(), row.mDefaultHeight) - row.mVerticalGap; 243 mGap = KeyboardParser.getDimensionOrFraction(keyboardAttr, 244 R.styleable.Keyboard_horizontalGap, 245 mKeyboard.getDisplayWidth(), row.mDefaultHorizontalGap); 246 keyWidth = KeyboardParser.getDimensionOrFraction(keyboardAttr, 247 R.styleable.Keyboard_keyWidth, 248 mKeyboard.getDisplayWidth(), row.mDefaultWidth); 249 } finally { 250 keyboardAttr.recycle(); 251 } 252 253 final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 254 R.styleable.Keyboard_Key); 255 try { 256 final KeyStyle style; 257 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) { 258 String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle); 259 style = keyStyles.getKeyStyle(styleName); 260 if (style == null) 261 throw new ParseException("Unknown key style: " + styleName, parser); 262 } else { 263 style = keyStyles.getEmptyKeyStyle(); 264 } 265 266 final int keyboardWidth = mKeyboard.getDisplayWidth(); 267 int keyXPos = KeyboardParser.getDimensionOrFraction(keyAttr, 268 R.styleable.Keyboard_Key_keyXPos, keyboardWidth, x); 269 if (keyXPos < 0) { 270 // If keyXPos is negative, the actual x-coordinate will be k + keyXPos. 271 keyXPos += keyboardWidth; 272 if (keyXPos < x) { 273 // keyXPos shouldn't be less than x because drawable area for this key starts 274 // at x. Or, this key will overlaps the adjacent key on its left hand side. 275 keyXPos = x; 276 } 277 } 278 if (keyWidth == KEYWIDTH_FILL_RIGHT) { 279 // If keyWidth is zero, the actual key width will be determined to fill out the 280 // area up to the right edge of the keyboard. 281 keyWidth = keyboardWidth - keyXPos; 282 } else if (keyWidth <= KEYWIDTH_FILL_BOTH) { 283 // If keyWidth is negative, the actual key width will be determined to fill out the 284 // area between the nearest key on the left hand side and the right edge of the 285 // keyboard. 286 keyXPos = x; 287 keyWidth = keyboardWidth - keyXPos; 288 } 289 290 // Horizontal gap is divided equally to both sides of the key. 291 mX = keyXPos + mGap / 2; 292 mY = y; 293 mWidth = keyWidth - mGap; 294 295 final CharSequence[] popupCharacters = style.getTextArray(keyAttr, 296 R.styleable.Keyboard_Key_popupCharacters); 297 // In Arabic symbol layouts, we'd like to keep digits in popup characters regardless of 298 // config_digit_popup_characters_enabled. 299 if (mKeyboard.mId.isAlphabetKeyboard() && !res.getBoolean( 300 R.bool.config_digit_popup_characters_enabled)) { 301 mPopupCharacters = filterOutDigitPopupCharacters(popupCharacters); 302 } else { 303 mPopupCharacters = popupCharacters; 304 } 305 mMaxPopupColumn = style.getInt(keyboardAttr, 306 R.styleable.Keyboard_Key_maxPopupKeyboardColumn, 307 mKeyboard.getMaxPopupKeyboardColumn()); 308 309 mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false); 310 mFunctional = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isFunctional, false); 311 mSticky = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isSticky, false); 312 mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true); 313 mEdgeFlags = 0; 314 315 final KeyboardIconsSet iconsSet = mKeyboard.mIconsSet; 316 mVisualInsetsLeft = KeyboardParser.getDimensionOrFraction(keyAttr, 317 R.styleable.Keyboard_Key_visualInsetsLeft, keyboardWidth, 0); 318 mVisualInsetsRight = KeyboardParser.getDimensionOrFraction(keyAttr, 319 R.styleable.Keyboard_Key_visualInsetsRight, keyboardWidth, 0); 320 mPreviewIcon = iconsSet.getIcon(style.getInt( 321 keyAttr, R.styleable.Keyboard_Key_keyIconPreview, 322 KeyboardIconsSet.ICON_UNDEFINED)); 323 Keyboard.setDefaultBounds(mPreviewIcon); 324 mIcon = iconsSet.getIcon(style.getInt( 325 keyAttr, R.styleable.Keyboard_Key_keyIcon, 326 KeyboardIconsSet.ICON_UNDEFINED)); 327 Keyboard.setDefaultBounds(mIcon); 328 final int shiftedIconId = style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted, 329 KeyboardIconsSet.ICON_UNDEFINED); 330 if (shiftedIconId != KeyboardIconsSet.ICON_UNDEFINED) { 331 final Drawable shiftedIcon = iconsSet.getIcon(shiftedIconId); 332 Keyboard.setDefaultBounds(shiftedIcon); 333 mKeyboard.addShiftedIcon(this, shiftedIcon); 334 } 335 mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel); 336 337 mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel); 338 mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0); 339 mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText); 340 // Choose the first letter of the label as primary code if not 341 // specified. 342 final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code, 343 Keyboard.CODE_UNSPECIFIED); 344 if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) { 345 final int firstChar = mLabel.charAt(0); 346 mCode = mKeyboard.isRtlKeyboard() ? getRtlParenthesisCode(firstChar) : firstChar; 347 } else if (code != Keyboard.CODE_UNSPECIFIED) { 348 mCode = code; 349 } else { 350 mCode = Keyboard.CODE_DUMMY; 351 } 352 if (mCode == Keyboard.CODE_SHIFT) { 353 mKeyboard.addShiftKey(this); 354 } 355 } finally { 356 keyAttr.recycle(); 357 } 358 } 359 360 public void addEdgeFlags(int flags) { 361 mEdgeFlags |= flags; 362 } 363 364 public CharSequence getCaseAdjustedLabel() { 365 return mKeyboard.adjustLabelCase(mLabel); 366 } 367 368 public Typeface selectTypeface(Typeface defaultTypeface) { 369 // TODO: Handle "bold" here too? 370 if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) { 371 return Typeface.DEFAULT; 372 } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) { 373 return Typeface.MONOSPACE; 374 } else { 375 return defaultTypeface; 376 } 377 } 378 379 public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) { 380 if (mLabel.length() > 1 381 && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO 382 | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) { 383 return label; 384 } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) { 385 return hintLabel; 386 } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) { 387 return largeLetter; 388 } else { 389 return letter; 390 } 391 } 392 393 public boolean hasPopupHint() { 394 return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0; 395 } 396 397 public boolean hasUppercaseLetter() { 398 return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0; 399 } 400 401 public boolean hasHintLabel() { 402 return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0; 403 } 404 405 private static boolean isDigitPopupCharacter(CharSequence label) { 406 return label != null && label.length() == 1 && Character.isDigit(label.charAt(0)); 407 } 408 409 private static CharSequence[] filterOutDigitPopupCharacters(CharSequence[] popupCharacters) { 410 if (popupCharacters == null || popupCharacters.length < 1) 411 return null; 412 if (popupCharacters.length == 1 && isDigitPopupCharacter( 413 PopupCharactersParser.getLabel(popupCharacters[0].toString()))) 414 return null; 415 ArrayList<CharSequence> filtered = null; 416 for (int i = 0; i < popupCharacters.length; i++) { 417 final CharSequence popupSpec = popupCharacters[i]; 418 if (isDigitPopupCharacter(PopupCharactersParser.getLabel(popupSpec.toString()))) { 419 if (filtered == null) { 420 filtered = new ArrayList<CharSequence>(); 421 for (int j = 0; j < i; j++) 422 filtered.add(popupCharacters[j]); 423 } 424 } else if (filtered != null) { 425 filtered.add(popupSpec); 426 } 427 } 428 if (filtered == null) 429 return popupCharacters; 430 if (filtered.size() == 0) 431 return null; 432 return filtered.toArray(new CharSequence[filtered.size()]); 433 } 434 435 public Drawable getIcon() { 436 return mIcon; 437 } 438 439 public Drawable getPreviewIcon() { 440 return mPreviewIcon; 441 } 442 443 public void setIcon(Drawable icon) { 444 mIcon = icon; 445 } 446 447 public void setPreviewIcon(Drawable icon) { 448 mPreviewIcon = icon; 449 } 450 451 /** 452 * Informs the key that it has been pressed, in case it needs to change its appearance or 453 * state. 454 * @see #onReleased() 455 */ 456 public void onPressed() { 457 mPressed = true; 458 } 459 460 /** 461 * Informs the key that it has been released, in case it needs to change its appearance or 462 * state. 463 * @see #onPressed() 464 */ 465 public void onReleased() { 466 mPressed = false; 467 } 468 469 public void setHighlightOn(boolean highlightOn) { 470 mHighlightOn = highlightOn; 471 } 472 473 public boolean isEnabled() { 474 return mEnabled; 475 } 476 477 public void setEnabled(boolean enabled) { 478 mEnabled = enabled; 479 } 480 481 /** 482 * Detects if a point falls on this key. 483 * @param x the x-coordinate of the point 484 * @param y the y-coordinate of the point 485 * @return whether or not the point falls on the key. If the key is attached to an edge, it will 486 * assume that all points between the key and the edge are considered to be on the key. 487 */ 488 public boolean isOnKey(int x, int y) { 489 final int left = mX - mGap / 2; 490 final int right = left + mWidth + mGap; 491 final int top = mY; 492 final int bottom = top + mHeight + mKeyboard.getVerticalGap(); 493 final int flags = mEdgeFlags; 494 if (flags == 0) { 495 return x >= left && x <= right && y >= top && y <= bottom; 496 } 497 final boolean leftEdge = (flags & Keyboard.EDGE_LEFT) != 0; 498 final boolean rightEdge = (flags & Keyboard.EDGE_RIGHT) != 0; 499 final boolean topEdge = (flags & Keyboard.EDGE_TOP) != 0; 500 final boolean bottomEdge = (flags & Keyboard.EDGE_BOTTOM) != 0; 501 // In order to mitigate rounding errors, we use (left <= x <= right) here. 502 return (x >= left || leftEdge) && (x <= right || rightEdge) 503 && (y >= top || topEdge) && (y <= bottom || bottomEdge); 504 } 505 506 /** 507 * Returns the square of the distance to the nearest edge of the key and the given point. 508 * @param x the x-coordinate of the point 509 * @param y the y-coordinate of the point 510 * @return the square of the distance of the point from the nearest edge of the key 511 */ 512 public int squaredDistanceToEdge(int x, int y) { 513 final int left = mX; 514 final int right = left + mWidth; 515 final int top = mY; 516 final int bottom = top + mHeight; 517 final int edgeX = x < left ? left : (x > right ? right : x); 518 final int edgeY = y < top ? top : (y > bottom ? bottom : y); 519 final int dx = x - edgeX; 520 final int dy = y - edgeY; 521 return dx * dx + dy * dy; 522 } 523 524 /** 525 * Returns the drawable state for the key, based on the current state and type of the key. 526 * @return the drawable state of the key. 527 * @see android.graphics.drawable.StateListDrawable#setState(int[]) 528 */ 529 public int[] getCurrentDrawableState() { 530 final boolean pressed = mPressed; 531 if (!mSticky && mFunctional) { 532 if (pressed) { 533 return KEY_STATE_FUNCTIONAL_PRESSED; 534 } else { 535 return KEY_STATE_FUNCTIONAL_NORMAL; 536 } 537 } 538 539 int[] states = KEY_STATE_NORMAL; 540 541 if (mHighlightOn) { 542 if (pressed) { 543 states = KEY_STATE_PRESSED_ON; 544 } else { 545 states = KEY_STATE_NORMAL_ON; 546 } 547 } else { 548 if (mSticky) { 549 if (pressed) { 550 states = KEY_STATE_PRESSED_OFF; 551 } else { 552 states = KEY_STATE_NORMAL_OFF; 553 } 554 } else { 555 if (pressed) { 556 states = KEY_STATE_PRESSED; 557 } 558 } 559 } 560 return states; 561 } 562} 563