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