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