1 /* 2 * Copyright (C) 2008 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.latin; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.content.res.XmlResourceParser; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.ColorFilter; 26 import android.graphics.Paint; 27 import android.graphics.Paint.Align; 28 import android.graphics.PixelFormat; 29 import android.graphics.PorterDuff; 30 import android.graphics.Rect; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.inputmethodservice.Keyboard; 34 import android.text.TextPaint; 35 import android.util.Log; 36 import android.view.ViewConfiguration; 37 import android.view.inputmethod.EditorInfo; 38 39 import java.util.List; 40 import java.util.Locale; 41 42 public class LatinKeyboard extends Keyboard { 43 44 private static final boolean DEBUG_PREFERRED_LETTER = false; 45 private static final String TAG = "LatinKeyboard"; 46 private static final int OPACITY_FULLY_OPAQUE = 255; 47 private static final int SPACE_LED_LENGTH_PERCENT = 80; 48 49 private Drawable mShiftLockIcon; 50 private Drawable mShiftLockPreviewIcon; 51 private Drawable mOldShiftIcon; 52 private Drawable mSpaceIcon; 53 private Drawable mSpaceAutoCompletionIndicator; 54 private Drawable mSpacePreviewIcon; 55 private Drawable mMicIcon; 56 private Drawable mMicPreviewIcon; 57 private Drawable m123MicIcon; 58 private Drawable m123MicPreviewIcon; 59 private final Drawable mButtonArrowLeftIcon; 60 private final Drawable mButtonArrowRightIcon; 61 private Key mShiftKey; 62 private Key mEnterKey; 63 private Key mF1Key; 64 private final Drawable mHintIcon; 65 private Key mSpaceKey; 66 private Key m123Key; 67 private final int NUMBER_HINT_COUNT = 10; 68 private Key[] mNumberHintKeys; 69 private Drawable[] mNumberHintIcons = new Drawable[NUMBER_HINT_COUNT]; 70 private int mSpaceKeyIndex = -1; 71 private int mSpaceDragStartX; 72 private int mSpaceDragLastDiff; 73 private Locale mLocale; 74 private LanguageSwitcher mLanguageSwitcher; 75 private final Resources mRes; 76 private final Context mContext; 77 private int mMode; 78 // Whether this keyboard has voice icon on it 79 private boolean mHasVoiceButton; 80 // Whether voice icon is enabled at all 81 private boolean mVoiceEnabled; 82 private final boolean mIsAlphaKeyboard; 83 private CharSequence m123Label; 84 private boolean mCurrentlyInSpace; 85 private SlidingLocaleDrawable mSlidingLocaleIcon; 86 private int[] mPrefLetterFrequencies; 87 private int mPrefLetter; 88 private int mPrefLetterX; 89 private int mPrefLetterY; 90 private int mPrefDistance; 91 92 // TODO: generalize for any keyboardId 93 private boolean mIsBlackSym; 94 95 // TODO: remove this attribute when either Keyboard.mDefaultVerticalGap or Key.parent becomes 96 // non-private. 97 private final int mVerticalGap; 98 99 private static final int SHIFT_OFF = 0; 100 private static final int SHIFT_ON = 1; 101 private static final int SHIFT_LOCKED = 2; 102 103 private int mShiftState = SHIFT_OFF; 104 105 private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; 106 private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; 107 private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f; 108 // Minimum width of space key preview (proportional to keyboard width) 109 private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f; 110 // Height in space key the language name will be drawn. (proportional to space key height) 111 private static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f; 112 // If the full language name needs to be smaller than this value to be drawn on space key, 113 // its short language name will be used instead. 114 private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f; 115 116 private static int sSpacebarVerticalCorrection; 117 118 public LatinKeyboard(Context context, int xmlLayoutResId) { 119 this(context, xmlLayoutResId, 0); 120 } 121 122 public LatinKeyboard(Context context, int xmlLayoutResId, int mode) { 123 super(context, xmlLayoutResId, mode); 124 final Resources res = context.getResources(); 125 mContext = context; 126 mMode = mode; 127 mRes = res; 128 mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); 129 mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); 130 setDefaultBounds(mShiftLockPreviewIcon); 131 mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); 132 mSpaceAutoCompletionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led); 133 mSpacePreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_space); 134 mMicIcon = res.getDrawable(R.drawable.sym_keyboard_mic); 135 mMicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_mic); 136 setDefaultBounds(mMicPreviewIcon); 137 mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); 138 mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); 139 m123MicIcon = res.getDrawable(R.drawable.sym_keyboard_123_mic); 140 m123MicPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_123_mic); 141 mHintIcon = res.getDrawable(R.drawable.hint_popup); 142 setDefaultBounds(m123MicPreviewIcon); 143 sSpacebarVerticalCorrection = res.getDimensionPixelOffset( 144 R.dimen.spacebar_vertical_correction); 145 mIsAlphaKeyboard = xmlLayoutResId == R.xml.kbd_qwerty 146 || xmlLayoutResId == R.xml.kbd_qwerty_black; 147 mSpaceKeyIndex = indexOf(LatinIME.KEYCODE_SPACE); 148 initializeNumberHintResources(context); 149 // TODO remove this initialization after cleanup 150 mVerticalGap = super.getVerticalGap(); 151 } 152 153 private void initializeNumberHintResources(Context context) { 154 final Resources res = context.getResources(); 155 mNumberHintIcons[0] = res.getDrawable(R.drawable.keyboard_hint_0); 156 mNumberHintIcons[1] = res.getDrawable(R.drawable.keyboard_hint_1); 157 mNumberHintIcons[2] = res.getDrawable(R.drawable.keyboard_hint_2); 158 mNumberHintIcons[3] = res.getDrawable(R.drawable.keyboard_hint_3); 159 mNumberHintIcons[4] = res.getDrawable(R.drawable.keyboard_hint_4); 160 mNumberHintIcons[5] = res.getDrawable(R.drawable.keyboard_hint_5); 161 mNumberHintIcons[6] = res.getDrawable(R.drawable.keyboard_hint_6); 162 mNumberHintIcons[7] = res.getDrawable(R.drawable.keyboard_hint_7); 163 mNumberHintIcons[8] = res.getDrawable(R.drawable.keyboard_hint_8); 164 mNumberHintIcons[9] = res.getDrawable(R.drawable.keyboard_hint_9); 165 } 166 167 @Override 168 protected Key createKeyFromXml(Resources res, Row parent, int x, int y, 169 XmlResourceParser parser) { 170 Key key = new LatinKey(res, parent, x, y, parser); 171 switch (key.codes[0]) { 172 case LatinIME.KEYCODE_ENTER: 173 mEnterKey = key; 174 break; 175 case LatinKeyboardView.KEYCODE_F1: 176 mF1Key = key; 177 break; 178 case LatinIME.KEYCODE_SPACE: 179 mSpaceKey = key; 180 break; 181 case KEYCODE_MODE_CHANGE: 182 m123Key = key; 183 m123Label = key.label; 184 break; 185 } 186 187 // For number hints on the upper-right corner of key 188 if (mNumberHintKeys == null) { 189 // NOTE: This protected method is being called from the base class constructor before 190 // mNumberHintKeys gets initialized. 191 mNumberHintKeys = new Key[NUMBER_HINT_COUNT]; 192 } 193 int hintNumber = -1; 194 if (LatinKeyboardBaseView.isNumberAtLeftmostPopupChar(key)) { 195 hintNumber = key.popupCharacters.charAt(0) - '0'; 196 } else if (LatinKeyboardBaseView.isNumberAtRightmostPopupChar(key)) { 197 hintNumber = key.popupCharacters.charAt(key.popupCharacters.length() - 1) - '0'; 198 } 199 if (hintNumber >= 0 && hintNumber <= 9) { 200 mNumberHintKeys[hintNumber] = key; 201 } 202 203 return key; 204 } 205 206 void setImeOptions(Resources res, int mode, int options) { 207 mMode = mode; 208 // TODO should clean up this method 209 if (mEnterKey != null) { 210 // Reset some of the rarely used attributes. 211 mEnterKey.popupCharacters = null; 212 mEnterKey.popupResId = 0; 213 mEnterKey.text = null; 214 switch (options&(EditorInfo.IME_MASK_ACTION|EditorInfo.IME_FLAG_NO_ENTER_ACTION)) { 215 case EditorInfo.IME_ACTION_GO: 216 mEnterKey.iconPreview = null; 217 mEnterKey.icon = null; 218 mEnterKey.label = res.getText(R.string.label_go_key); 219 break; 220 case EditorInfo.IME_ACTION_NEXT: 221 mEnterKey.iconPreview = null; 222 mEnterKey.icon = null; 223 mEnterKey.label = res.getText(R.string.label_next_key); 224 break; 225 case EditorInfo.IME_ACTION_DONE: 226 mEnterKey.iconPreview = null; 227 mEnterKey.icon = null; 228 mEnterKey.label = res.getText(R.string.label_done_key); 229 break; 230 case EditorInfo.IME_ACTION_SEARCH: 231 mEnterKey.iconPreview = res.getDrawable( 232 R.drawable.sym_keyboard_feedback_search); 233 mEnterKey.icon = res.getDrawable(mIsBlackSym ? 234 R.drawable.sym_bkeyboard_search : R.drawable.sym_keyboard_search); 235 mEnterKey.label = null; 236 break; 237 case EditorInfo.IME_ACTION_SEND: 238 mEnterKey.iconPreview = null; 239 mEnterKey.icon = null; 240 mEnterKey.label = res.getText(R.string.label_send_key); 241 break; 242 default: 243 if (mode == KeyboardSwitcher.MODE_IM) { 244 mEnterKey.icon = mHintIcon; 245 mEnterKey.iconPreview = null; 246 mEnterKey.label = ":-)"; 247 mEnterKey.text = ":-) "; 248 mEnterKey.popupResId = R.xml.popup_smileys; 249 } else { 250 mEnterKey.iconPreview = res.getDrawable( 251 R.drawable.sym_keyboard_feedback_return); 252 mEnterKey.icon = res.getDrawable(mIsBlackSym ? 253 R.drawable.sym_bkeyboard_return : R.drawable.sym_keyboard_return); 254 mEnterKey.label = null; 255 } 256 break; 257 } 258 // Set the initial size of the preview icon 259 if (mEnterKey.iconPreview != null) { 260 setDefaultBounds(mEnterKey.iconPreview); 261 } 262 } 263 } 264 265 void enableShiftLock() { 266 int index = getShiftKeyIndex(); 267 if (index >= 0) { 268 mShiftKey = getKeys().get(index); 269 if (mShiftKey instanceof LatinKey) { 270 ((LatinKey)mShiftKey).enableShiftLock(); 271 } 272 mOldShiftIcon = mShiftKey.icon; 273 } 274 } 275 276 void setShiftLocked(boolean shiftLocked) { 277 if (mShiftKey != null) { 278 if (shiftLocked) { 279 mShiftKey.on = true; 280 mShiftKey.icon = mShiftLockIcon; 281 mShiftState = SHIFT_LOCKED; 282 } else { 283 mShiftKey.on = false; 284 mShiftKey.icon = mShiftLockIcon; 285 mShiftState = SHIFT_ON; 286 } 287 } 288 } 289 290 boolean isShiftLocked() { 291 return mShiftState == SHIFT_LOCKED; 292 } 293 294 @Override 295 public boolean setShifted(boolean shiftState) { 296 boolean shiftChanged = false; 297 if (mShiftKey != null) { 298 if (shiftState == false) { 299 shiftChanged = mShiftState != SHIFT_OFF; 300 mShiftState = SHIFT_OFF; 301 mShiftKey.on = false; 302 mShiftKey.icon = mOldShiftIcon; 303 } else { 304 if (mShiftState == SHIFT_OFF) { 305 shiftChanged = mShiftState == SHIFT_OFF; 306 mShiftState = SHIFT_ON; 307 mShiftKey.icon = mShiftLockIcon; 308 } 309 } 310 } else { 311 return super.setShifted(shiftState); 312 } 313 return shiftChanged; 314 } 315 316 @Override 317 public boolean isShifted() { 318 if (mShiftKey != null) { 319 return mShiftState != SHIFT_OFF; 320 } else { 321 return super.isShifted(); 322 } 323 } 324 325 /* package */ boolean isAlphaKeyboard() { 326 return mIsAlphaKeyboard; 327 } 328 329 public void setColorOfSymbolIcons(boolean isAutoCompletion, boolean isBlack) { 330 mIsBlackSym = isBlack; 331 if (isBlack) { 332 mShiftLockIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_shift_locked); 333 mSpaceIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_space); 334 mMicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_mic); 335 m123MicIcon = mRes.getDrawable(R.drawable.sym_bkeyboard_123_mic); 336 } else { 337 mShiftLockIcon = mRes.getDrawable(R.drawable.sym_keyboard_shift_locked); 338 mSpaceIcon = mRes.getDrawable(R.drawable.sym_keyboard_space); 339 mMicIcon = mRes.getDrawable(R.drawable.sym_keyboard_mic); 340 m123MicIcon = mRes.getDrawable(R.drawable.sym_keyboard_123_mic); 341 } 342 updateDynamicKeys(); 343 if (mSpaceKey != null) { 344 updateSpaceBarForLocale(isAutoCompletion, isBlack); 345 } 346 updateNumberHintKeys(); 347 } 348 349 private void setDefaultBounds(Drawable drawable) { 350 drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 351 } 352 353 public void setVoiceMode(boolean hasVoiceButton, boolean hasVoice) { 354 mHasVoiceButton = hasVoiceButton; 355 mVoiceEnabled = hasVoice; 356 updateDynamicKeys(); 357 } 358 359 private void updateDynamicKeys() { 360 update123Key(); 361 updateF1Key(); 362 } 363 364 private void update123Key() { 365 // Update KEYCODE_MODE_CHANGE key only on alphabet mode, not on symbol mode. 366 if (m123Key != null && mIsAlphaKeyboard) { 367 if (mVoiceEnabled && !mHasVoiceButton) { 368 m123Key.icon = m123MicIcon; 369 m123Key.iconPreview = m123MicPreviewIcon; 370 m123Key.label = null; 371 } else { 372 m123Key.icon = null; 373 m123Key.iconPreview = null; 374 m123Key.label = m123Label; 375 } 376 } 377 } 378 379 private void updateF1Key() { 380 // Update KEYCODE_F1 key. Please note that some keyboard layouts have no F1 key. 381 if (mF1Key == null) 382 return; 383 384 if (mIsAlphaKeyboard) { 385 if (mMode == KeyboardSwitcher.MODE_URL) { 386 setNonMicF1Key(mF1Key, "/", R.xml.popup_slash); 387 } else if (mMode == KeyboardSwitcher.MODE_EMAIL) { 388 setNonMicF1Key(mF1Key, "@", R.xml.popup_at); 389 } else { 390 if (mVoiceEnabled && mHasVoiceButton) { 391 setMicF1Key(mF1Key); 392 } else { 393 setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); 394 } 395 } 396 } else { // Symbols keyboard 397 if (mVoiceEnabled && mHasVoiceButton) { 398 setMicF1Key(mF1Key); 399 } else { 400 setNonMicF1Key(mF1Key, ",", R.xml.popup_comma); 401 } 402 } 403 } 404 405 private void setMicF1Key(Key key) { 406 // HACK: draw mMicIcon and mHintIcon at the same time 407 final Drawable micWithSettingsHintDrawable = new BitmapDrawable(mRes, 408 drawSynthesizedSettingsHintImage(key.width, key.height, mMicIcon, mHintIcon)); 409 410 key.label = null; 411 key.codes = new int[] { LatinKeyboardView.KEYCODE_VOICE }; 412 key.popupResId = R.xml.popup_mic; 413 key.icon = micWithSettingsHintDrawable; 414 key.iconPreview = mMicPreviewIcon; 415 } 416 417 private void setNonMicF1Key(Key key, String label, int popupResId) { 418 key.label = label; 419 key.codes = new int[] { label.charAt(0) }; 420 key.popupResId = popupResId; 421 key.icon = mHintIcon; 422 key.iconPreview = null; 423 } 424 425 public boolean isF1Key(Key key) { 426 return key == mF1Key; 427 } 428 429 public static boolean hasPuncOrSmileysPopup(Key key) { 430 return key.popupResId == R.xml.popup_punctuation || key.popupResId == R.xml.popup_smileys; 431 } 432 433 /** 434 * @return a key which should be invalidated. 435 */ 436 public Key onAutoCompletionStateChanged(boolean isAutoCompletion) { 437 updateSpaceBarForLocale(isAutoCompletion, mIsBlackSym); 438 return mSpaceKey; 439 } 440 441 private void updateNumberHintKeys() { 442 for (int i = 0; i < mNumberHintKeys.length; ++i) { 443 if (mNumberHintKeys[i] != null) { 444 mNumberHintKeys[i].icon = mNumberHintIcons[i]; 445 } 446 } 447 } 448 449 public boolean isLanguageSwitchEnabled() { 450 return mLocale != null; 451 } 452 453 private void updateSpaceBarForLocale(boolean isAutoCompletion, boolean isBlack) { 454 // If application locales are explicitly selected. 455 if (mLocale != null) { 456 mSpaceKey.icon = new BitmapDrawable(mRes, 457 drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); 458 } else { 459 // sym_keyboard_space_led can be shared with Black and White symbol themes. 460 if (isAutoCompletion) { 461 mSpaceKey.icon = new BitmapDrawable(mRes, 462 drawSpaceBar(OPACITY_FULLY_OPAQUE, isAutoCompletion, isBlack)); 463 } else { 464 mSpaceKey.icon = isBlack ? mRes.getDrawable(R.drawable.sym_bkeyboard_space) 465 : mRes.getDrawable(R.drawable.sym_keyboard_space); 466 } 467 } 468 } 469 470 // Compute width of text with specified text size using paint. 471 private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) { 472 paint.setTextSize(textSize); 473 paint.getTextBounds(text, 0, text.length(), bounds); 474 return bounds.width(); 475 } 476 477 // Overlay two images: mainIcon and hintIcon. 478 private Bitmap drawSynthesizedSettingsHintImage( 479 int width, int height, Drawable mainIcon, Drawable hintIcon) { 480 if (mainIcon == null || hintIcon == null) 481 return null; 482 Rect hintIconPadding = new Rect(0, 0, 0, 0); 483 hintIcon.getPadding(hintIconPadding); 484 final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 485 final Canvas canvas = new Canvas(buffer); 486 canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); 487 488 // Draw main icon at the center of the key visual 489 // Assuming the hintIcon shares the same padding with the key's background drawable 490 final int drawableX = (width + hintIconPadding.left - hintIconPadding.right 491 - mainIcon.getIntrinsicWidth()) / 2; 492 final int drawableY = (height + hintIconPadding.top - hintIconPadding.bottom 493 - mainIcon.getIntrinsicHeight()) / 2; 494 setDefaultBounds(mainIcon); 495 canvas.translate(drawableX, drawableY); 496 mainIcon.draw(canvas); 497 canvas.translate(-drawableX, -drawableY); 498 499 // Draw hint icon fully in the key 500 hintIcon.setBounds(0, 0, width, height); 501 hintIcon.draw(canvas); 502 return buffer; 503 } 504 505 // Layout local language name and left and right arrow on space bar. 506 private static String layoutSpaceBar(Paint paint, Locale locale, Drawable lArrow, 507 Drawable rArrow, int width, int height, float origTextSize, 508 boolean allowVariableTextSize) { 509 final float arrowWidth = lArrow.getIntrinsicWidth(); 510 final float arrowHeight = lArrow.getIntrinsicHeight(); 511 final float maxTextWidth = width - (arrowWidth + arrowWidth); 512 final Rect bounds = new Rect(); 513 514 // Estimate appropriate language name text size to fit in maxTextWidth. 515 String language = LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); 516 int textWidth = getTextWidth(paint, language, origTextSize, bounds); 517 // Assuming text width and text size are proportional to each other. 518 float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); 519 520 final boolean useShortName; 521 if (allowVariableTextSize) { 522 textWidth = getTextWidth(paint, language, textSize, bounds); 523 // If text size goes too small or text does not fit, use short name 524 useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME 525 || textWidth > maxTextWidth; 526 } else { 527 useShortName = textWidth > maxTextWidth; 528 textSize = origTextSize; 529 } 530 if (useShortName) { 531 language = LanguageSwitcher.toTitleCase(locale.getLanguage()); 532 textWidth = getTextWidth(paint, language, origTextSize, bounds); 533 textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); 534 } 535 paint.setTextSize(textSize); 536 537 // Place left and right arrow just before and after language text. 538 final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; 539 final int top = (int)(baseline - arrowHeight); 540 final float remains = (width - textWidth) / 2; 541 lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline); 542 rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth), 543 (int)baseline); 544 545 return language; 546 } 547 548 private Bitmap drawSpaceBar(int opacity, boolean isAutoCompletion, boolean isBlack) { 549 final int width = mSpaceKey.width; 550 final int height = mSpaceIcon.getIntrinsicHeight(); 551 final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 552 final Canvas canvas = new Canvas(buffer); 553 canvas.drawColor(mRes.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); 554 555 // If application locales are explicitly selected. 556 if (mLocale != null) { 557 final Paint paint = new Paint(); 558 paint.setAlpha(opacity); 559 paint.setAntiAlias(true); 560 paint.setTextAlign(Align.CENTER); 561 562 final boolean allowVariableTextSize = true; 563 final String language = layoutSpaceBar(paint, mLanguageSwitcher.getInputLocale(), 564 mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height, 565 getTextSizeFromTheme(android.R.style.TextAppearance_Small, 14), 566 allowVariableTextSize); 567 568 // Draw language text with shadow 569 final int shadowColor = mRes.getColor(isBlack 570 ? R.color.latinkeyboard_bar_language_shadow_black 571 : R.color.latinkeyboard_bar_language_shadow_white); 572 final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; 573 final float descent = paint.descent(); 574 paint.setColor(shadowColor); 575 canvas.drawText(language, width / 2, baseline - descent - 1, paint); 576 paint.setColor(mRes.getColor(R.color.latinkeyboard_bar_language_text)); 577 canvas.drawText(language, width / 2, baseline - descent, paint); 578 579 // Put arrows that are already layed out on either side of the text 580 if (mLanguageSwitcher.getLocaleCount() > 1) { 581 mButtonArrowLeftIcon.draw(canvas); 582 mButtonArrowRightIcon.draw(canvas); 583 } 584 } 585 586 // Draw the spacebar icon at the bottom 587 if (isAutoCompletion) { 588 final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; 589 final int iconHeight = mSpaceAutoCompletionIndicator.getIntrinsicHeight(); 590 int x = (width - iconWidth) / 2; 591 int y = height - iconHeight; 592 mSpaceAutoCompletionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight); 593 mSpaceAutoCompletionIndicator.draw(canvas); 594 } else { 595 final int iconWidth = mSpaceIcon.getIntrinsicWidth(); 596 final int iconHeight = mSpaceIcon.getIntrinsicHeight(); 597 int x = (width - iconWidth) / 2; 598 int y = height - iconHeight; 599 mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight); 600 mSpaceIcon.draw(canvas); 601 } 602 return buffer; 603 } 604 605 private void updateLocaleDrag(int diff) { 606 if (mSlidingLocaleIcon == null) { 607 final int width = Math.max(mSpaceKey.width, 608 (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO)); 609 final int height = mSpacePreviewIcon.getIntrinsicHeight(); 610 mSlidingLocaleIcon = new SlidingLocaleDrawable(mSpacePreviewIcon, width, height); 611 mSlidingLocaleIcon.setBounds(0, 0, width, height); 612 mSpaceKey.iconPreview = mSlidingLocaleIcon; 613 } 614 mSlidingLocaleIcon.setDiff(diff); 615 if (Math.abs(diff) == Integer.MAX_VALUE) { 616 mSpaceKey.iconPreview = mSpacePreviewIcon; 617 } else { 618 mSpaceKey.iconPreview = mSlidingLocaleIcon; 619 } 620 mSpaceKey.iconPreview.invalidateSelf(); 621 } 622 623 public int getLanguageChangeDirection() { 624 if (mSpaceKey == null || mLanguageSwitcher.getLocaleCount() < 2 625 || Math.abs(mSpaceDragLastDiff) < mSpaceKey.width * SPACEBAR_DRAG_THRESHOLD ) { 626 return 0; // No change 627 } 628 return mSpaceDragLastDiff > 0 ? 1 : -1; 629 } 630 631 public void setLanguageSwitcher(LanguageSwitcher switcher, boolean isAutoCompletion, 632 boolean isBlackSym) { 633 mLanguageSwitcher = switcher; 634 Locale locale = mLanguageSwitcher.getLocaleCount() > 0 635 ? mLanguageSwitcher.getInputLocale() 636 : null; 637 // If the language count is 1 and is the same as the system language, don't show it. 638 if (locale != null 639 && mLanguageSwitcher.getLocaleCount() == 1 640 && mLanguageSwitcher.getSystemLocale().getLanguage() 641 .equalsIgnoreCase(locale.getLanguage())) { 642 locale = null; 643 } 644 mLocale = locale; 645 setColorOfSymbolIcons(isAutoCompletion, isBlackSym); 646 } 647 648 boolean isCurrentlyInSpace() { 649 return mCurrentlyInSpace; 650 } 651 652 void setPreferredLetters(int[] frequencies) { 653 mPrefLetterFrequencies = frequencies; 654 mPrefLetter = 0; 655 } 656 657 void keyReleased() { 658 mCurrentlyInSpace = false; 659 mSpaceDragLastDiff = 0; 660 mPrefLetter = 0; 661 mPrefLetterX = 0; 662 mPrefLetterY = 0; 663 mPrefDistance = Integer.MAX_VALUE; 664 if (mSpaceKey != null) { 665 updateLocaleDrag(Integer.MAX_VALUE); 666 } 667 } 668 669 /** 670 * Does the magic of locking the touch gesture into the spacebar when 671 * switching input languages. 672 */ 673 boolean isInside(LatinKey key, int x, int y) { 674 final int code = key.codes[0]; 675 if (code == KEYCODE_SHIFT || 676 code == KEYCODE_DELETE) { 677 y -= key.height / 10; 678 if (code == KEYCODE_SHIFT) x += key.width / 6; 679 if (code == KEYCODE_DELETE) x -= key.width / 6; 680 } else if (code == LatinIME.KEYCODE_SPACE) { 681 y += LatinKeyboard.sSpacebarVerticalCorrection; 682 if (mLanguageSwitcher.getLocaleCount() > 1) { 683 if (mCurrentlyInSpace) { 684 int diff = x - mSpaceDragStartX; 685 if (Math.abs(diff - mSpaceDragLastDiff) > 0) { 686 updateLocaleDrag(diff); 687 } 688 mSpaceDragLastDiff = diff; 689 return true; 690 } else { 691 boolean insideSpace = key.isInsideSuper(x, y); 692 if (insideSpace) { 693 mCurrentlyInSpace = true; 694 mSpaceDragStartX = x; 695 updateLocaleDrag(0); 696 } 697 return insideSpace; 698 } 699 } 700 } else if (mPrefLetterFrequencies != null) { 701 // New coordinate? Reset 702 if (mPrefLetterX != x || mPrefLetterY != y) { 703 mPrefLetter = 0; 704 mPrefDistance = Integer.MAX_VALUE; 705 } 706 // Handle preferred next letter 707 final int[] pref = mPrefLetterFrequencies; 708 if (mPrefLetter > 0) { 709 if (DEBUG_PREFERRED_LETTER) { 710 if (mPrefLetter == code && !key.isInsideSuper(x, y)) { 711 Log.d(TAG, "CORRECTED !!!!!!"); 712 } 713 } 714 return mPrefLetter == code; 715 } else { 716 final boolean inside = key.isInsideSuper(x, y); 717 int[] nearby = getNearestKeys(x, y); 718 List<Key> nearbyKeys = getKeys(); 719 if (inside) { 720 // If it's a preferred letter 721 if (inPrefList(code, pref)) { 722 // Check if its frequency is much lower than a nearby key 723 mPrefLetter = code; 724 mPrefLetterX = x; 725 mPrefLetterY = y; 726 for (int i = 0; i < nearby.length; i++) { 727 Key k = nearbyKeys.get(nearby[i]); 728 if (k != key && inPrefList(k.codes[0], pref)) { 729 final int dist = distanceFrom(k, x, y); 730 if (dist < (int) (k.width * OVERLAP_PERCENTAGE_LOW_PROB) && 731 (pref[k.codes[0]] > pref[mPrefLetter] * 3)) { 732 mPrefLetter = k.codes[0]; 733 mPrefDistance = dist; 734 if (DEBUG_PREFERRED_LETTER) { 735 Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!"); 736 } 737 break; 738 } 739 } 740 } 741 742 return mPrefLetter == code; 743 } 744 } 745 746 // Get the surrounding keys and intersect with the preferred list 747 // For all in the intersection 748 // if distance from touch point is within a reasonable distance 749 // make this the pref letter 750 // If no pref letter 751 // return inside; 752 // else return thiskey == prefletter; 753 754 for (int i = 0; i < nearby.length; i++) { 755 Key k = nearbyKeys.get(nearby[i]); 756 if (inPrefList(k.codes[0], pref)) { 757 final int dist = distanceFrom(k, x, y); 758 if (dist < (int) (k.width * OVERLAP_PERCENTAGE_HIGH_PROB) 759 && dist < mPrefDistance) { 760 mPrefLetter = k.codes[0]; 761 mPrefLetterX = x; 762 mPrefLetterY = y; 763 mPrefDistance = dist; 764 } 765 } 766 } 767 // Didn't find any 768 if (mPrefLetter == 0) { 769 return inside; 770 } else { 771 return mPrefLetter == code; 772 } 773 } 774 } 775 776 // Lock into the spacebar 777 if (mCurrentlyInSpace) return false; 778 779 return key.isInsideSuper(x, y); 780 } 781 782 private boolean inPrefList(int code, int[] pref) { 783 if (code < pref.length && code >= 0) return pref[code] > 0; 784 return false; 785 } 786 787 private int distanceFrom(Key k, int x, int y) { 788 if (y > k.y && y < k.y + k.height) { 789 return Math.abs(k.x + k.width / 2 - x); 790 } else { 791 return Integer.MAX_VALUE; 792 } 793 } 794 795 @Override 796 public int[] getNearestKeys(int x, int y) { 797 if (mCurrentlyInSpace) { 798 return new int[] { mSpaceKeyIndex }; 799 } else { 800 // Avoid dead pixels at edges of the keyboard 801 return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)), 802 Math.max(0, Math.min(y, getHeight() - 1))); 803 } 804 } 805 806 private int indexOf(int code) { 807 List<Key> keys = getKeys(); 808 int count = keys.size(); 809 for (int i = 0; i < count; i++) { 810 if (keys.get(i).codes[0] == code) return i; 811 } 812 return -1; 813 } 814 815 private int getTextSizeFromTheme(int style, int defValue) { 816 TypedArray array = mContext.getTheme().obtainStyledAttributes( 817 style, new int[] { android.R.attr.textSize }); 818 int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue); 819 return textSize; 820 } 821 822 // TODO LatinKey could be static class 823 class LatinKey extends Keyboard.Key { 824 825 // functional normal state (with properties) 826 private final int[] KEY_STATE_FUNCTIONAL_NORMAL = { 827 android.R.attr.state_single 828 }; 829 830 // functional pressed state (with properties) 831 private final int[] KEY_STATE_FUNCTIONAL_PRESSED = { 832 android.R.attr.state_single, 833 android.R.attr.state_pressed 834 }; 835 836 private boolean mShiftLockEnabled; 837 838 public LatinKey(Resources res, Keyboard.Row parent, int x, int y, 839 XmlResourceParser parser) { 840 super(res, parent, x, y, parser); 841 if (popupCharacters != null && popupCharacters.length() == 0) { 842 // If there is a keyboard with no keys specified in popupCharacters 843 popupResId = 0; 844 } 845 } 846 847 private void enableShiftLock() { 848 mShiftLockEnabled = true; 849 } 850 851 // sticky is used for shift key. If a key is not sticky and is modifier, 852 // the key will be treated as functional. 853 private boolean isFunctionalKey() { 854 return !sticky && modifier; 855 } 856 857 @Override 858 public void onReleased(boolean inside) { 859 if (!mShiftLockEnabled) { 860 super.onReleased(inside); 861 } else { 862 pressed = !pressed; 863 } 864 } 865 866 /** 867 * Overriding this method so that we can reduce the target area for certain keys. 868 */ 869 @Override 870 public boolean isInside(int x, int y) { 871 // TODO This should be done by parent.isInside(this, x, y) 872 // if Key.parent were protected. 873 boolean result = LatinKeyboard.this.isInside(this, x, y); 874 return result; 875 } 876 877 boolean isInsideSuper(int x, int y) { 878 return super.isInside(x, y); 879 } 880 881 @Override 882 public int[] getCurrentDrawableState() { 883 if (isFunctionalKey()) { 884 if (pressed) { 885 return KEY_STATE_FUNCTIONAL_PRESSED; 886 } else { 887 return KEY_STATE_FUNCTIONAL_NORMAL; 888 } 889 } 890 return super.getCurrentDrawableState(); 891 } 892 893 @Override 894 public int squaredDistanceFrom(int x, int y) { 895 // We should count vertical gap between rows to calculate the center of this Key. 896 final int verticalGap = LatinKeyboard.this.mVerticalGap; 897 final int xDist = this.x + width / 2 - x; 898 final int yDist = this.y + (height + verticalGap) / 2 - y; 899 return xDist * xDist + yDist * yDist; 900 } 901 } 902 903 /** 904 * Animation to be displayed on the spacebar preview popup when switching 905 * languages by swiping the spacebar. It draws the current, previous and 906 * next languages and moves them by the delta of touch movement on the spacebar. 907 */ 908 class SlidingLocaleDrawable extends Drawable { 909 910 private final int mWidth; 911 private final int mHeight; 912 private final Drawable mBackground; 913 private final TextPaint mTextPaint; 914 private final int mMiddleX; 915 private final Drawable mLeftDrawable; 916 private final Drawable mRightDrawable; 917 private final int mThreshold; 918 private int mDiff; 919 private boolean mHitThreshold; 920 private String mCurrentLanguage; 921 private String mNextLanguage; 922 private String mPrevLanguage; 923 924 public SlidingLocaleDrawable(Drawable background, int width, int height) { 925 mBackground = background; 926 setDefaultBounds(mBackground); 927 mWidth = width; 928 mHeight = height; 929 mTextPaint = new TextPaint(); 930 mTextPaint.setTextSize(getTextSizeFromTheme(android.R.style.TextAppearance_Medium, 18)); 931 mTextPaint.setColor(R.color.latinkeyboard_transparent); 932 mTextPaint.setTextAlign(Align.CENTER); 933 mTextPaint.setAlpha(OPACITY_FULLY_OPAQUE); 934 mTextPaint.setAntiAlias(true); 935 mMiddleX = (mWidth - mBackground.getIntrinsicWidth()) / 2; 936 mLeftDrawable = 937 mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_left); 938 mRightDrawable = 939 mRes.getDrawable(R.drawable.sym_keyboard_feedback_language_arrows_right); 940 mThreshold = ViewConfiguration.get(mContext).getScaledTouchSlop(); 941 } 942 943 private void setDiff(int diff) { 944 if (diff == Integer.MAX_VALUE) { 945 mHitThreshold = false; 946 mCurrentLanguage = null; 947 return; 948 } 949 mDiff = diff; 950 if (mDiff > mWidth) mDiff = mWidth; 951 if (mDiff < -mWidth) mDiff = -mWidth; 952 if (Math.abs(mDiff) > mThreshold) mHitThreshold = true; 953 invalidateSelf(); 954 } 955 956 private String getLanguageName(Locale locale) { 957 return LanguageSwitcher.toTitleCase(locale.getDisplayLanguage(locale)); 958 } 959 960 @Override 961 public void draw(Canvas canvas) { 962 canvas.save(); 963 if (mHitThreshold) { 964 Paint paint = mTextPaint; 965 final int width = mWidth; 966 final int height = mHeight; 967 final int diff = mDiff; 968 final Drawable lArrow = mLeftDrawable; 969 final Drawable rArrow = mRightDrawable; 970 canvas.clipRect(0, 0, width, height); 971 if (mCurrentLanguage == null) { 972 final LanguageSwitcher languageSwitcher = mLanguageSwitcher; 973 mCurrentLanguage = getLanguageName(languageSwitcher.getInputLocale()); 974 mNextLanguage = getLanguageName(languageSwitcher.getNextInputLocale()); 975 mPrevLanguage = getLanguageName(languageSwitcher.getPrevInputLocale()); 976 } 977 // Draw language text with shadow 978 final float baseline = mHeight * SPACEBAR_LANGUAGE_BASELINE - paint.descent(); 979 paint.setColor(mRes.getColor(R.color.latinkeyboard_feedback_language_text)); 980 canvas.drawText(mCurrentLanguage, width / 2 + diff, baseline, paint); 981 canvas.drawText(mNextLanguage, diff - width / 2, baseline, paint); 982 canvas.drawText(mPrevLanguage, diff + width + width / 2, baseline, paint); 983 984 setDefaultBounds(lArrow); 985 rArrow.setBounds(width - rArrow.getIntrinsicWidth(), 0, width, 986 rArrow.getIntrinsicHeight()); 987 lArrow.draw(canvas); 988 rArrow.draw(canvas); 989 } 990 if (mBackground != null) { 991 canvas.translate(mMiddleX, 0); 992 mBackground.draw(canvas); 993 } 994 canvas.restore(); 995 } 996 997 @Override 998 public int getOpacity() { 999 return PixelFormat.TRANSLUCENT; 1000 } 1001 1002 @Override 1003 public void setAlpha(int alpha) { 1004 // Ignore 1005 } 1006 1007 @Override 1008 public void setColorFilter(ColorFilter cf) { 1009 // Ignore 1010 } 1011 1012 @Override 1013 public int getIntrinsicWidth() { 1014 return mWidth; 1015 } 1016 1017 @Override 1018 public int getIntrinsicHeight() { 1019 return mHeight; 1020 } 1021 } 1022 } 1023