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 17 package com.android.inputmethod.keyboard; 18 19 import android.content.res.Resources; 20 import android.content.res.TypedArray; 21 import android.graphics.Rect; 22 import android.graphics.Typeface; 23 import android.graphics.drawable.Drawable; 24 import android.text.TextUtils; 25 import android.util.Xml; 26 27 import com.android.inputmethod.keyboard.internal.KeyStyles; 28 import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle; 29 import com.android.inputmethod.keyboard.internal.KeyboardBuilder; 30 import com.android.inputmethod.keyboard.internal.KeyboardBuilder.ParseException; 31 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet; 32 import com.android.inputmethod.keyboard.internal.KeyboardParams; 33 import com.android.inputmethod.keyboard.internal.MoreKeySpecParser; 34 import com.android.inputmethod.latin.R; 35 36 import org.xmlpull.v1.XmlPullParser; 37 38 import java.util.HashMap; 39 import java.util.Map; 40 41 /** 42 * Class for describing the position and characteristics of a single key in the keyboard. 43 */ 44 public class Key { 45 /** 46 * The key code (unicode or custom code) that this key generates. 47 */ 48 public final int mCode; 49 50 /** Label to display */ 51 public final CharSequence mLabel; 52 /** Hint label to display on the key in conjunction with the label */ 53 public final CharSequence mHintLabel; 54 /** Option of the label */ 55 private final int mLabelOption; 56 private static final int LABEL_OPTION_ALIGN_LEFT = 0x01; 57 private static final int LABEL_OPTION_ALIGN_RIGHT = 0x02; 58 private static final int LABEL_OPTION_ALIGN_LEFT_OF_CENTER = 0x08; 59 private static final int LABEL_OPTION_LARGE_LETTER = 0x10; 60 private static final int LABEL_OPTION_FONT_NORMAL = 0x20; 61 private static final int LABEL_OPTION_FONT_MONO_SPACE = 0x40; 62 private static final int LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO = 0x80; 63 private static final int LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO = 0x100; 64 private static final int LABEL_OPTION_HAS_POPUP_HINT = 0x200; 65 private static final int LABEL_OPTION_HAS_UPPERCASE_LETTER = 0x400; 66 private static final int LABEL_OPTION_HAS_HINT_LABEL = 0x800; 67 private static final int LABEL_OPTION_WITH_ICON_LEFT = 0x1000; 68 private static final int LABEL_OPTION_WITH_ICON_RIGHT = 0x2000; 69 private static final int LABEL_OPTION_AUTO_X_SCALE = 0x4000; 70 71 /** Icon to display instead of a label. Icon takes precedence over a label */ 72 private Drawable mIcon; 73 /** Preview version of the icon, for the preview popup */ 74 private Drawable mPreviewIcon; 75 76 /** Width of the key, not including the gap */ 77 public final int mWidth; 78 /** Height of the key, not including the gap */ 79 public final int mHeight; 80 /** The horizontal gap around this key */ 81 public final int mHorizontalGap; 82 /** The vertical gap below this key */ 83 public final int mVerticalGap; 84 /** The visual insets */ 85 public final int mVisualInsetsLeft; 86 public final int mVisualInsetsRight; 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 /** Hit bounding box of the key */ 92 public final Rect mHitBox = new Rect(); 93 94 /** Text to output when pressed. This can be multiple characters, like ".com" */ 95 public final CharSequence mOutputText; 96 /** More keys */ 97 public final CharSequence[] mMoreKeys; 98 /** More keys maximum column number */ 99 public final int mMaxMoreKeysColumn; 100 101 /** Background type that represents different key background visual than normal one. */ 102 public final int mBackgroundType; 103 public static final int BACKGROUND_TYPE_NORMAL = 0; 104 public static final int BACKGROUND_TYPE_FUNCTIONAL = 1; 105 public static final int BACKGROUND_TYPE_ACTION = 2; 106 public static final int BACKGROUND_TYPE_STICKY = 3; 107 108 /** Whether this key repeats itself when held down */ 109 public final boolean mRepeatable; 110 111 /** The current pressed state of this key */ 112 private boolean mPressed; 113 /** If this is a sticky key, is its highlight on? */ 114 private boolean mHighlightOn; 115 /** Key is enabled and responds on press */ 116 private boolean mEnabled = true; 117 /** Whether this key needs to show the "..." popup hint for special purposes */ 118 private boolean mNeedsSpecialPopupHint; 119 120 // RTL parenthesis character swapping map. 121 private static final Map<Integer, Integer> sRtlParenthesisMap = new HashMap<Integer, Integer>(); 122 123 static { 124 // The all letters need to be mirrored are found at 125 // http://www.unicode.org/Public/6.0.0/ucd/extracted/DerivedBinaryProperties.txt 126 addRtlParenthesisPair('(', ')'); 127 addRtlParenthesisPair('[', ']'); 128 addRtlParenthesisPair('{', '}'); 129 addRtlParenthesisPair('<', '>'); 130 // \u00ab: LEFT-POINTING DOUBLE ANGLE QUOTATION MARK 131 // \u00bb: RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK 132 addRtlParenthesisPair('\u00ab', '\u00bb'); 133 // \u2039: SINGLE LEFT-POINTING ANGLE QUOTATION MARK 134 // \u203a: SINGLE RIGHT-POINTING ANGLE QUOTATION MARK 135 addRtlParenthesisPair('\u2039', '\u203a'); 136 // \u2264: LESS-THAN OR EQUAL TO 137 // \u2265: GREATER-THAN OR EQUAL TO 138 addRtlParenthesisPair('\u2264', '\u2265'); 139 } 140 141 private static void addRtlParenthesisPair(int left, int right) { 142 sRtlParenthesisMap.put(left, right); 143 sRtlParenthesisMap.put(right, left); 144 } 145 146 public static int getRtlParenthesisCode(int code, boolean isRtl) { 147 if (isRtl && sRtlParenthesisMap.containsKey(code)) { 148 return sRtlParenthesisMap.get(code); 149 } else { 150 return code; 151 } 152 } 153 154 private static int getCode(Resources res, KeyboardParams params, String moreKeySpec) { 155 return getRtlParenthesisCode( 156 MoreKeySpecParser.getCode(res, moreKeySpec), params.mIsRtlKeyboard); 157 } 158 159 private static Drawable getIcon(KeyboardParams params, String moreKeySpec) { 160 return params.mIconsSet.getIcon(MoreKeySpecParser.getIconId(moreKeySpec)); 161 } 162 163 /** 164 * This constructor is being used only for key in more keys keyboard. 165 */ 166 public Key(Resources res, KeyboardParams params, String moreKeySpec, 167 int x, int y, int width, int height) { 168 this(params, MoreKeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec), 169 getCode(res, params, moreKeySpec), MoreKeySpecParser.getOutputText(moreKeySpec), 170 x, y, width, height); 171 } 172 173 /** 174 * This constructor is being used only for key in popup suggestions pane. 175 */ 176 public Key(KeyboardParams params, CharSequence label, CharSequence hintLabel, Drawable icon, 177 int code, CharSequence outputText, int x, int y, int width, int height) { 178 mHeight = height - params.mVerticalGap; 179 mHorizontalGap = params.mHorizontalGap; 180 mVerticalGap = params.mVerticalGap; 181 mVisualInsetsLeft = mVisualInsetsRight = 0; 182 mWidth = width - mHorizontalGap; 183 mHintLabel = hintLabel; 184 mLabelOption = 0; 185 mBackgroundType = BACKGROUND_TYPE_NORMAL; 186 mRepeatable = false; 187 mMoreKeys = null; 188 mMaxMoreKeysColumn = 0; 189 mLabel = label; 190 mOutputText = outputText; 191 mCode = code; 192 mIcon = icon; 193 // Horizontal gap is divided equally to both sides of the key. 194 mX = x + mHorizontalGap / 2; 195 mY = y; 196 mHitBox.set(x, y, x + width + 1, y + height); 197 } 198 199 /** 200 * Create a key with the given top-left coordinate and extract its attributes from the XML 201 * parser. 202 * @param res resources associated with the caller's context 203 * @param params the keyboard building parameters. 204 * @param row the row that this key belongs to. row's x-coordinate will be the right edge of 205 * this key. 206 * @param parser the XML parser containing the attributes for this key 207 * @param keyStyles active key styles set 208 */ 209 public Key(Resources res, KeyboardParams params, KeyboardBuilder.Row row, 210 XmlPullParser parser, KeyStyles keyStyles) { 211 final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap; 212 final int keyHeight = row.mRowHeight; 213 mVerticalGap = params.mVerticalGap; 214 mHeight = keyHeight - mVerticalGap; 215 216 final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser), 217 R.styleable.Keyboard_Key); 218 219 final KeyStyle style; 220 if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyStyle)) { 221 String styleName = keyAttr.getString(R.styleable.Keyboard_Key_keyStyle); 222 style = keyStyles.getKeyStyle(styleName); 223 if (style == null) 224 throw new ParseException("Unknown key style: " + styleName, parser); 225 } else { 226 style = keyStyles.getEmptyKeyStyle(); 227 } 228 229 final float keyXPos = row.getKeyX(keyAttr); 230 final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); 231 final int keyYPos = row.getKeyY(); 232 233 // Horizontal gap is divided equally to both sides of the key. 234 mX = (int) (keyXPos + horizontalGap / 2); 235 mY = keyYPos; 236 mWidth = (int) (keyWidth - horizontalGap); 237 mHorizontalGap = (int) horizontalGap; 238 mHitBox.set((int)keyXPos, keyYPos, (int)(keyXPos + keyWidth) + 1, keyYPos + keyHeight); 239 // Update row to have current x coordinate. 240 row.setXPos(keyXPos + keyWidth); 241 242 final CharSequence[] moreKeys = style.getTextArray(keyAttr, 243 R.styleable.Keyboard_Key_moreKeys); 244 // In Arabic symbol layouts, we'd like to keep digits in more keys regardless of 245 // config_digit_more_keys_enabled. 246 if (params.mId.isAlphabetKeyboard() 247 && !res.getBoolean(R.bool.config_digit_more_keys_enabled)) { 248 mMoreKeys = MoreKeySpecParser.filterOut(res, moreKeys, MoreKeySpecParser.DIGIT_FILTER); 249 } else { 250 mMoreKeys = moreKeys; 251 } 252 mMaxMoreKeysColumn = style.getInt(keyAttr, 253 R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMiniKeyboardColumn); 254 255 mBackgroundType = style.getInt(keyAttr, 256 R.styleable.Keyboard_Key_backgroundType, BACKGROUND_TYPE_NORMAL); 257 mRepeatable = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_isRepeatable, false); 258 mEnabled = style.getBoolean(keyAttr, R.styleable.Keyboard_Key_enabled, true); 259 260 final KeyboardIconsSet iconsSet = params.mIconsSet; 261 mVisualInsetsLeft = (int) KeyboardBuilder.getDimensionOrFraction(keyAttr, 262 R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0); 263 mVisualInsetsRight = (int) KeyboardBuilder.getDimensionOrFraction(keyAttr, 264 R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0); 265 mPreviewIcon = iconsSet.getIcon(style.getInt(keyAttr, 266 R.styleable.Keyboard_Key_keyIconPreview, KeyboardIconsSet.ICON_UNDEFINED)); 267 mIcon = iconsSet.getIcon(style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIcon, 268 KeyboardIconsSet.ICON_UNDEFINED)); 269 final int shiftedIconId = style.getInt(keyAttr, R.styleable.Keyboard_Key_keyIconShifted, 270 KeyboardIconsSet.ICON_UNDEFINED); 271 if (shiftedIconId != KeyboardIconsSet.ICON_UNDEFINED) { 272 final Drawable shiftedIcon = iconsSet.getIcon(shiftedIconId); 273 params.addShiftedIcon(this, shiftedIcon); 274 } 275 mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel); 276 277 mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel); 278 mLabelOption = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyLabelOption, 0); 279 mOutputText = style.getText(keyAttr, R.styleable.Keyboard_Key_keyOutputText); 280 // Choose the first letter of the label as primary code if not 281 // specified. 282 final int code = style.getInt(keyAttr, R.styleable.Keyboard_Key_code, 283 Keyboard.CODE_UNSPECIFIED); 284 if (code == Keyboard.CODE_UNSPECIFIED && !TextUtils.isEmpty(mLabel)) { 285 final int firstChar = mLabel.charAt(0); 286 mCode = getRtlParenthesisCode(firstChar, params.mIsRtlKeyboard); 287 } else if (code != Keyboard.CODE_UNSPECIFIED) { 288 mCode = code; 289 } else { 290 mCode = Keyboard.CODE_DUMMY; 291 } 292 293 keyAttr.recycle(); 294 } 295 296 public void markAsLeftEdge(KeyboardParams params) { 297 mHitBox.left = params.mHorizontalEdgesPadding; 298 } 299 300 public void markAsRightEdge(KeyboardParams params) { 301 mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding; 302 } 303 304 public void markAsTopEdge(KeyboardParams params) { 305 mHitBox.top = params.mTopPadding; 306 } 307 308 public void markAsBottomEdge(KeyboardParams params) { 309 mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding; 310 } 311 312 public boolean isSticky() { 313 return mBackgroundType == BACKGROUND_TYPE_STICKY; 314 } 315 316 public boolean isSpacer() { 317 return false; 318 } 319 320 public Typeface selectTypeface(Typeface defaultTypeface) { 321 // TODO: Handle "bold" here too? 322 if ((mLabelOption & LABEL_OPTION_FONT_NORMAL) != 0) { 323 return Typeface.DEFAULT; 324 } else if ((mLabelOption & LABEL_OPTION_FONT_MONO_SPACE) != 0) { 325 return Typeface.MONOSPACE; 326 } else { 327 return defaultTypeface; 328 } 329 } 330 331 public int selectTextSize(int letter, int largeLetter, int label, int hintLabel) { 332 if (mLabel.length() > 1 333 && (mLabelOption & (LABEL_OPTION_FOLLOW_KEY_LETTER_RATIO 334 | LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO)) == 0) { 335 return label; 336 } else if ((mLabelOption & LABEL_OPTION_FOLLOW_KEY_HINT_LABEL_RATIO) != 0) { 337 return hintLabel; 338 } else if ((mLabelOption & LABEL_OPTION_LARGE_LETTER) != 0) { 339 return largeLetter; 340 } else { 341 return letter; 342 } 343 } 344 345 public boolean isAlignLeft() { 346 return (mLabelOption & LABEL_OPTION_ALIGN_LEFT) != 0; 347 } 348 349 public boolean isAlignRight() { 350 return (mLabelOption & LABEL_OPTION_ALIGN_RIGHT) != 0; 351 } 352 353 public boolean isAlignLeftOfCenter() { 354 return (mLabelOption & LABEL_OPTION_ALIGN_LEFT_OF_CENTER) != 0; 355 } 356 357 public boolean hasPopupHint() { 358 return (mLabelOption & LABEL_OPTION_HAS_POPUP_HINT) != 0; 359 } 360 361 public void setNeedsSpecialPopupHint(boolean needsSpecialPopupHint) { 362 mNeedsSpecialPopupHint = needsSpecialPopupHint; 363 } 364 365 public boolean needsSpecialPopupHint() { 366 return mNeedsSpecialPopupHint; 367 } 368 369 public boolean hasUppercaseLetter() { 370 return (mLabelOption & LABEL_OPTION_HAS_UPPERCASE_LETTER) != 0; 371 } 372 373 public boolean hasHintLabel() { 374 return (mLabelOption & LABEL_OPTION_HAS_HINT_LABEL) != 0; 375 } 376 377 public boolean hasLabelWithIconLeft() { 378 return (mLabelOption & LABEL_OPTION_WITH_ICON_LEFT) != 0; 379 } 380 381 public boolean hasLabelWithIconRight() { 382 return (mLabelOption & LABEL_OPTION_WITH_ICON_RIGHT) != 0; 383 } 384 385 public boolean needsXScale() { 386 return (mLabelOption & LABEL_OPTION_AUTO_X_SCALE) != 0; 387 } 388 389 public Drawable getIcon() { 390 return mIcon; 391 } 392 393 public Drawable getPreviewIcon() { 394 return mPreviewIcon; 395 } 396 397 public void setIcon(Drawable icon) { 398 mIcon = icon; 399 } 400 401 public void setPreviewIcon(Drawable icon) { 402 mPreviewIcon = icon; 403 } 404 405 /** 406 * Informs the key that it has been pressed, in case it needs to change its appearance or 407 * state. 408 * @see #onReleased() 409 */ 410 public void onPressed() { 411 mPressed = true; 412 } 413 414 /** 415 * Informs the key that it has been released, in case it needs to change its appearance or 416 * state. 417 * @see #onPressed() 418 */ 419 public void onReleased() { 420 mPressed = false; 421 } 422 423 public void setHighlightOn(boolean highlightOn) { 424 mHighlightOn = highlightOn; 425 } 426 427 public boolean isEnabled() { 428 return mEnabled; 429 } 430 431 public void setEnabled(boolean enabled) { 432 mEnabled = enabled; 433 } 434 435 /** 436 * Detects if a point falls on this key. 437 * @param x the x-coordinate of the point 438 * @param y the y-coordinate of the point 439 * @return whether or not the point falls on the key. If the key is attached to an edge, it will 440 * assume that all points between the key and the edge are considered to be on the key. 441 * @see {@link #markAsLeftEdge(KeyboardParams)} etc. 442 */ 443 public boolean isOnKey(int x, int y) { 444 return mHitBox.contains(x, y); 445 } 446 447 /** 448 * Returns the square of the distance to the nearest edge of the key and the given point. 449 * @param x the x-coordinate of the point 450 * @param y the y-coordinate of the point 451 * @return the square of the distance of the point from the nearest edge of the key 452 */ 453 public int squaredDistanceToEdge(int x, int y) { 454 final int left = mX; 455 final int right = left + mWidth; 456 final int top = mY; 457 final int bottom = top + mHeight; 458 final int edgeX = x < left ? left : (x > right ? right : x); 459 final int edgeY = y < top ? top : (y > bottom ? bottom : y); 460 final int dx = x - edgeX; 461 final int dy = y - edgeY; 462 return dx * dx + dy * dy; 463 } 464 465 private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_ON = { 466 android.R.attr.state_checkable, 467 android.R.attr.state_checked 468 }; 469 470 private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_ON = { 471 android.R.attr.state_pressed, 472 android.R.attr.state_checkable, 473 android.R.attr.state_checked 474 }; 475 476 private final static int[] KEY_STATE_NORMAL_HIGHLIGHT_OFF = { 477 android.R.attr.state_checkable 478 }; 479 480 private final static int[] KEY_STATE_PRESSED_HIGHLIGHT_OFF = { 481 android.R.attr.state_pressed, 482 android.R.attr.state_checkable 483 }; 484 485 private final static int[] KEY_STATE_NORMAL = { 486 }; 487 488 private final static int[] KEY_STATE_PRESSED = { 489 android.R.attr.state_pressed 490 }; 491 492 // functional normal state (with properties) 493 private static final int[] KEY_STATE_FUNCTIONAL_NORMAL = { 494 android.R.attr.state_single 495 }; 496 497 // functional pressed state (with properties) 498 private static final int[] KEY_STATE_FUNCTIONAL_PRESSED = { 499 android.R.attr.state_single, 500 android.R.attr.state_pressed 501 }; 502 503 // action normal state (with properties) 504 private static final int[] KEY_STATE_ACTIVE_NORMAL = { 505 android.R.attr.state_active 506 }; 507 508 // action pressed state (with properties) 509 private static final int[] KEY_STATE_ACTIVE_PRESSED = { 510 android.R.attr.state_active, 511 android.R.attr.state_pressed 512 }; 513 514 /** 515 * Returns the drawable state for the key, based on the current state and type of the key. 516 * @return the drawable state of the key. 517 * @see android.graphics.drawable.StateListDrawable#setState(int[]) 518 */ 519 public int[] getCurrentDrawableState() { 520 final boolean pressed = mPressed; 521 522 switch (mBackgroundType) { 523 case BACKGROUND_TYPE_FUNCTIONAL: 524 return pressed ? KEY_STATE_FUNCTIONAL_PRESSED : KEY_STATE_FUNCTIONAL_NORMAL; 525 case BACKGROUND_TYPE_ACTION: 526 return pressed ? KEY_STATE_ACTIVE_PRESSED : KEY_STATE_ACTIVE_NORMAL; 527 case BACKGROUND_TYPE_STICKY: 528 if (mHighlightOn) { 529 return pressed ? KEY_STATE_PRESSED_HIGHLIGHT_ON : KEY_STATE_NORMAL_HIGHLIGHT_ON; 530 } else { 531 return pressed ? KEY_STATE_PRESSED_HIGHLIGHT_OFF : KEY_STATE_NORMAL_HIGHLIGHT_OFF; 532 } 533 default: /* BACKGROUND_TYPE_NORMAL */ 534 return pressed ? KEY_STATE_PRESSED : KEY_STATE_NORMAL; 535 } 536 } 537 538 public static class Spacer extends Key { 539 public Spacer(Resources res, KeyboardParams params, KeyboardBuilder.Row row, 540 XmlPullParser parser, KeyStyles keyStyles) { 541 super(res, params, row, parser, keyStyles); 542 } 543 544 /** 545 * This constructor is being used only for divider in more keys keyboard. 546 */ 547 public Spacer(KeyboardParams params, Drawable icon, int x, int y, int width, int height) { 548 super(params, null, null, icon, Keyboard.CODE_DUMMY, null, x, y, width, height); 549 } 550 551 @Override 552 public boolean isSpacer() { 553 return true; 554 } 555 } 556 } 557