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