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