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