LatinKeyboard.java revision 4d3b9d709c36a4c5ea0705ccc3d58c28fbf873d0
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 17package com.android.inputmethod.keyboard; 18 19import com.android.inputmethod.latin.R; 20import com.android.inputmethod.latin.SubtypeSwitcher; 21 22import android.content.Context; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Bitmap; 26import android.graphics.Canvas; 27import android.graphics.Color; 28import android.graphics.Paint; 29import android.graphics.Paint.Align; 30import android.graphics.PorterDuff; 31import android.graphics.Rect; 32import android.graphics.drawable.BitmapDrawable; 33import android.graphics.drawable.Drawable; 34import android.util.Log; 35 36import java.util.List; 37import java.util.Locale; 38 39// TODO: We should remove this class 40public class LatinKeyboard extends Keyboard { 41 42 private static final boolean DEBUG_PREFERRED_LETTER = false; 43 private static final String TAG = "LatinKeyboard"; 44 45 public static final int OPACITY_FULLY_OPAQUE = 255; 46 private static final int SPACE_LED_LENGTH_PERCENT = 80; 47 48 private final Context mContext; 49 50 /* Space key and its icons, drawables and colors. */ 51 private final Key mSpaceKey; 52 private final Drawable mSpaceIcon; 53 private final Drawable mSpacePreviewIcon; 54 private final int[] mSpaceKeyIndexArray; 55 private final Drawable mSpaceAutoCorrectionIndicator; 56 private final Drawable mButtonArrowLeftIcon; 57 private final Drawable mButtonArrowRightIcon; 58 private final int mSpacebarTextColor; 59 private final int mSpacebarTextShadowColor; 60 private final int mSpacebarVerticalCorrection; 61 private float mSpacebarTextFadeFactor = 0.0f; 62 private int mSpaceDragStartX; 63 private int mSpaceDragLastDiff; 64 private boolean mCurrentlyInSpace; 65 private SlidingLocaleDrawable mSlidingLocaleIcon; 66 67 /* Shortcut key and its icons if available */ 68 private final Key mShortcutKey; 69 private final Drawable mEnabledShortcutIcon; 70 private final Drawable mDisabledShortcutIcon; 71 72 private int[] mPrefLetterFrequencies; 73 private int mPrefLetter; 74 private int mPrefLetterX; 75 private int mPrefLetterY; 76 private int mPrefDistance; 77 78 private static final float SPACEBAR_DRAG_THRESHOLD = 0.8f; 79 private static final float OVERLAP_PERCENTAGE_LOW_PROB = 0.70f; 80 private static final float OVERLAP_PERCENTAGE_HIGH_PROB = 0.85f; 81 // Minimum width of space key preview (proportional to keyboard width) 82 private static final float SPACEBAR_POPUP_MIN_RATIO = 0.4f; 83 // Height in space key the language name will be drawn. (proportional to space key height) 84 public static final float SPACEBAR_LANGUAGE_BASELINE = 0.6f; 85 // If the full language name needs to be smaller than this value to be drawn on space key, 86 // its short language name will be used instead. 87 private static final float MINIMUM_SCALE_OF_LANGUAGE_NAME = 0.8f; 88 89 private static final String SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "small"; 90 private static final String MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR = "medium"; 91 92 public LatinKeyboard(Context context, KeyboardId id) { 93 super(context, id.getXmlId(), id); 94 final Resources res = context.getResources(); 95 mContext = context; 96 97 final List<Key> keys = getKeys(); 98 int spaceKeyIndex = -1; 99 int shortcutKeyIndex = -1; 100 final int keyCount = keys.size(); 101 for (int index = 0; index < keyCount; index++) { 102 // For now, assuming there are up to one space key and one shortcut key respectively. 103 switch (keys.get(index).mCode) { 104 case CODE_SPACE: 105 spaceKeyIndex = index; 106 break; 107 case CODE_VOICE: 108 shortcutKeyIndex = index; 109 break; 110 } 111 } 112 113 // The index of space key is available only after Keyboard constructor has finished. 114 mSpaceKey = (spaceKeyIndex >= 0) ? keys.get(spaceKeyIndex) : null; 115 mSpaceIcon = (mSpaceKey != null) ? mSpaceKey.getIcon() : null; 116 mSpacePreviewIcon = (mSpaceKey != null) ? mSpaceKey.getPreviewIcon() : null; 117 mSpaceKeyIndexArray = new int[] { spaceKeyIndex }; 118 119 mShortcutKey = (shortcutKeyIndex >= 0) ? keys.get(shortcutKeyIndex) : null; 120 mEnabledShortcutIcon = (mShortcutKey != null) ? mShortcutKey.getIcon() : null; 121 122 mSpacebarTextColor = res.getColor(R.color.latinkeyboard_bar_language_text); 123 if (id.mColorScheme == KeyboardView.COLOR_SCHEME_BLACK) { 124 mSpacebarTextShadowColor = res.getColor( 125 R.color.latinkeyboard_bar_language_shadow_black); 126 mDisabledShortcutIcon = res.getDrawable(R.drawable.sym_bkeyboard_voice_off); 127 } else { // default color scheme is KeyboardView.COLOR_SCHEME_WHITE 128 mSpacebarTextShadowColor = res.getColor( 129 R.color.latinkeyboard_bar_language_shadow_white); 130 mDisabledShortcutIcon = res.getDrawable(R.drawable.sym_keyboard_voice_off_holo); 131 } 132 mSpaceAutoCorrectionIndicator = res.getDrawable(R.drawable.sym_keyboard_space_led); 133 mButtonArrowLeftIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_left); 134 mButtonArrowRightIcon = res.getDrawable(R.drawable.sym_keyboard_language_arrows_right); 135 mSpacebarVerticalCorrection = res.getDimensionPixelOffset( 136 R.dimen.spacebar_vertical_correction); 137 } 138 139 public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboardView view) { 140 mSpacebarTextFadeFactor = fadeFactor; 141 updateSpacebarForLocale(false); 142 if (view != null) 143 view.invalidateKey(mSpaceKey); 144 } 145 146 private static int getSpacebarTextColor(int color, float fadeFactor) { 147 final int newColor = Color.argb((int)(Color.alpha(color) * fadeFactor), 148 Color.red(color), Color.green(color), Color.blue(color)); 149 return newColor; 150 } 151 152 public void updateShortcutKey(boolean available, LatinKeyboardView view) { 153 if (mShortcutKey == null) 154 return; 155 mShortcutKey.mEnabled = available; 156 mShortcutKey.setIcon(available ? mEnabledShortcutIcon : mDisabledShortcutIcon); 157 if (view != null) 158 view.invalidateKey(mShortcutKey); 159 } 160 161 /** 162 * @return a key which should be invalidated. 163 */ 164 public Key onAutoCorrectionStateChanged(boolean isAutoCorrection) { 165 updateSpacebarForLocale(isAutoCorrection); 166 return mSpaceKey; 167 } 168 169 private void updateSpacebarForLocale(boolean isAutoCorrection) { 170 final Resources res = mContext.getResources(); 171 // If application locales are explicitly selected. 172 if (SubtypeSwitcher.getInstance().needsToDisplayLanguage()) { 173 mSpaceKey.setIcon(new BitmapDrawable(res, 174 drawSpacebar(OPACITY_FULLY_OPAQUE, isAutoCorrection))); 175 } else { 176 // sym_keyboard_space_led can be shared with Black and White symbol themes. 177 if (isAutoCorrection) { 178 mSpaceKey.setIcon(new BitmapDrawable(res, 179 drawSpacebar(OPACITY_FULLY_OPAQUE, isAutoCorrection))); 180 } else { 181 mSpaceKey.setIcon(mSpaceIcon); 182 } 183 } 184 } 185 186 // Compute width of text with specified text size using paint. 187 private static int getTextWidth(Paint paint, String text, float textSize, Rect bounds) { 188 paint.setTextSize(textSize); 189 paint.getTextBounds(text, 0, text.length(), bounds); 190 return bounds.width(); 191 } 192 193 // Layout local language name and left and right arrow on spacebar. 194 private static String layoutSpacebar(Paint paint, Locale locale, Drawable lArrow, 195 Drawable rArrow, int width, int height, float origTextSize, 196 boolean allowVariableTextSize) { 197 final float arrowWidth = lArrow.getIntrinsicWidth(); 198 final float arrowHeight = lArrow.getIntrinsicHeight(); 199 final float maxTextWidth = width - (arrowWidth + arrowWidth); 200 final Rect bounds = new Rect(); 201 202 // Estimate appropriate language name text size to fit in maxTextWidth. 203 String language = SubtypeSwitcher.getFullDisplayName(locale, true); 204 int textWidth = getTextWidth(paint, language, origTextSize, bounds); 205 // Assuming text width and text size are proportional to each other. 206 float textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); 207 208 final boolean useShortName; 209 if (allowVariableTextSize) { 210 textWidth = getTextWidth(paint, language, textSize, bounds); 211 // If text size goes too small or text does not fit, use short name 212 useShortName = textSize / origTextSize < MINIMUM_SCALE_OF_LANGUAGE_NAME 213 || textWidth > maxTextWidth; 214 } else { 215 useShortName = textWidth > maxTextWidth; 216 textSize = origTextSize; 217 } 218 if (useShortName) { 219 language = SubtypeSwitcher.getShortDisplayLanguage(locale); 220 textWidth = getTextWidth(paint, language, origTextSize, bounds); 221 textSize = origTextSize * Math.min(maxTextWidth / textWidth, 1.0f); 222 } 223 paint.setTextSize(textSize); 224 225 // Place left and right arrow just before and after language text. 226 final float baseline = height * SPACEBAR_LANGUAGE_BASELINE; 227 final int top = (int)(baseline - arrowHeight); 228 final float remains = (width - textWidth) / 2; 229 lArrow.setBounds((int)(remains - arrowWidth), top, (int)remains, (int)baseline); 230 rArrow.setBounds((int)(remains + textWidth), top, (int)(remains + textWidth + arrowWidth), 231 (int)baseline); 232 233 return language; 234 } 235 236 private Bitmap drawSpacebar(int opacity, boolean isAutoCorrection) { 237 final int width = mSpaceKey.mWidth; 238 final int height = mSpaceIcon != null ? mSpaceIcon.getIntrinsicHeight() : mSpaceKey.mHeight; 239 final Bitmap buffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 240 final Canvas canvas = new Canvas(buffer); 241 final Resources res = mContext.getResources(); 242 canvas.drawColor(res.getColor(R.color.latinkeyboard_transparent), PorterDuff.Mode.CLEAR); 243 244 SubtypeSwitcher subtypeSwitcher = SubtypeSwitcher.getInstance(); 245 // If application locales are explicitly selected. 246 if (subtypeSwitcher.needsToDisplayLanguage()) { 247 final Paint paint = new Paint(); 248 paint.setAlpha(opacity); 249 paint.setAntiAlias(true); 250 paint.setTextAlign(Align.CENTER); 251 252 final String textSizeOfLanguageOnSpacebar = res.getString( 253 R.string.config_text_size_of_language_on_spacebar, 254 SMALL_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR); 255 final int textStyle; 256 final int defaultTextSize; 257 if (MEDIUM_TEXT_SIZE_OF_LANGUAGE_ON_SPACEBAR.equals(textSizeOfLanguageOnSpacebar)) { 258 textStyle = android.R.style.TextAppearance_Medium; 259 defaultTextSize = 18; 260 } else { 261 textStyle = android.R.style.TextAppearance_Small; 262 defaultTextSize = 14; 263 } 264 265 final boolean allowVariableTextSize = true; 266 final String language = layoutSpacebar(paint, subtypeSwitcher.getInputLocale(), 267 mButtonArrowLeftIcon, mButtonArrowRightIcon, width, height, 268 getTextSizeFromTheme(textStyle, defaultTextSize), 269 allowVariableTextSize); 270 271 // Draw language text with shadow 272 // In case there is no space icon, we will place the language text at the center of 273 // spacebar. 274 final float descent = paint.descent(); 275 final float textHeight = -paint.ascent() + descent; 276 final float baseline = (mSpaceIcon != null) ? height * SPACEBAR_LANGUAGE_BASELINE 277 : height / 2 + textHeight / 2; 278 paint.setColor(getSpacebarTextColor(mSpacebarTextShadowColor, mSpacebarTextFadeFactor)); 279 canvas.drawText(language, width / 2, baseline - descent - 1, paint); 280 paint.setColor(getSpacebarTextColor(mSpacebarTextColor, mSpacebarTextFadeFactor)); 281 canvas.drawText(language, width / 2, baseline - descent, paint); 282 283 // Put arrows that are already layed out on either side of the text 284 if (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher() 285 && subtypeSwitcher.getEnabledKeyboardLocaleCount() > 1) { 286 mButtonArrowLeftIcon.draw(canvas); 287 mButtonArrowRightIcon.draw(canvas); 288 } 289 } 290 291 // Draw the spacebar icon at the bottom 292 if (isAutoCorrection) { 293 final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100; 294 final int iconHeight = mSpaceAutoCorrectionIndicator.getIntrinsicHeight(); 295 int x = (width - iconWidth) / 2; 296 int y = height - iconHeight; 297 mSpaceAutoCorrectionIndicator.setBounds(x, y, x + iconWidth, y + iconHeight); 298 mSpaceAutoCorrectionIndicator.draw(canvas); 299 } else if (mSpaceIcon != null) { 300 final int iconWidth = mSpaceIcon.getIntrinsicWidth(); 301 final int iconHeight = mSpaceIcon.getIntrinsicHeight(); 302 int x = (width - iconWidth) / 2; 303 int y = height - iconHeight; 304 mSpaceIcon.setBounds(x, y, x + iconWidth, y + iconHeight); 305 mSpaceIcon.draw(canvas); 306 } 307 return buffer; 308 } 309 310 private void updateLocaleDrag(int diff) { 311 if (mSlidingLocaleIcon == null) { 312 final int width = Math.max(mSpaceKey.mWidth, 313 (int)(getMinWidth() * SPACEBAR_POPUP_MIN_RATIO)); 314 final int height = mSpacePreviewIcon.getIntrinsicHeight(); 315 mSlidingLocaleIcon = 316 new SlidingLocaleDrawable(mContext, mSpacePreviewIcon, width, height); 317 mSlidingLocaleIcon.setBounds(0, 0, width, height); 318 mSpaceKey.setPreviewIcon(mSlidingLocaleIcon); 319 } 320 mSlidingLocaleIcon.setDiff(diff); 321 if (Math.abs(diff) == Integer.MAX_VALUE) { 322 mSpaceKey.setPreviewIcon(mSpacePreviewIcon); 323 } else { 324 mSpaceKey.setPreviewIcon(mSlidingLocaleIcon); 325 } 326 mSpaceKey.getPreviewIcon().invalidateSelf(); 327 } 328 329 public int getLanguageChangeDirection() { 330 if (mSpaceKey == null || SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() <= 1 331 || Math.abs(mSpaceDragLastDiff) < mSpaceKey.mWidth * SPACEBAR_DRAG_THRESHOLD) { 332 return 0; // No change 333 } 334 return mSpaceDragLastDiff > 0 ? 1 : -1; 335 } 336 337 public void setPreferredLetters(int[] frequencies) { 338 mPrefLetterFrequencies = frequencies; 339 mPrefLetter = 0; 340 } 341 342 public void keyReleased() { 343 mCurrentlyInSpace = false; 344 mSpaceDragLastDiff = 0; 345 mPrefLetter = 0; 346 mPrefLetterX = 0; 347 mPrefLetterY = 0; 348 mPrefDistance = Integer.MAX_VALUE; 349 if (mSpaceKey != null) { 350 updateLocaleDrag(Integer.MAX_VALUE); 351 } 352 } 353 354 /** 355 * Does the magic of locking the touch gesture into the spacebar when 356 * switching input languages. 357 */ 358 @Override 359 public boolean isInside(Key key, int pointX, int pointY) { 360 int x = pointX; 361 int y = pointY; 362 final int code = key.mCode; 363 if (code == CODE_SPACE) { 364 y += mSpacebarVerticalCorrection; 365 if (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher() 366 && SubtypeSwitcher.getInstance().getEnabledKeyboardLocaleCount() > 1) { 367 if (mCurrentlyInSpace) { 368 int diff = x - mSpaceDragStartX; 369 if (Math.abs(diff - mSpaceDragLastDiff) > 0) { 370 updateLocaleDrag(diff); 371 } 372 mSpaceDragLastDiff = diff; 373 return true; 374 } else { 375 boolean isOnSpace = key.isOnKey(x, y); 376 if (isOnSpace) { 377 mCurrentlyInSpace = true; 378 mSpaceDragStartX = x; 379 updateLocaleDrag(0); 380 } 381 return isOnSpace; 382 } 383 } 384 } else if (mPrefLetterFrequencies != null) { 385 // New coordinate? Reset 386 if (mPrefLetterX != x || mPrefLetterY != y) { 387 mPrefLetter = 0; 388 mPrefDistance = Integer.MAX_VALUE; 389 } 390 // Handle preferred next letter 391 final int[] pref = mPrefLetterFrequencies; 392 if (mPrefLetter > 0) { 393 if (DEBUG_PREFERRED_LETTER) { 394 if (mPrefLetter == code && !key.isOnKey(x, y)) { 395 Log.d(TAG, "CORRECTED !!!!!!"); 396 } 397 } 398 return mPrefLetter == code; 399 } else { 400 final boolean isOnKey = key.isOnKey(x, y); 401 int[] nearby = getNearestKeys(x, y); 402 List<Key> nearbyKeys = getKeys(); 403 if (isOnKey) { 404 // If it's a preferred letter 405 if (inPrefList(code, pref)) { 406 // Check if its frequency is much lower than a nearby key 407 mPrefLetter = code; 408 mPrefLetterX = x; 409 mPrefLetterY = y; 410 for (int i = 0; i < nearby.length; i++) { 411 Key k = nearbyKeys.get(nearby[i]); 412 if (k != key && inPrefList(k.mCode, pref)) { 413 final int dist = distanceFrom(k, x, y); 414 if (dist < (int) (k.mWidth * OVERLAP_PERCENTAGE_LOW_PROB) && 415 (pref[k.mCode] > pref[mPrefLetter] * 3)) { 416 mPrefLetter = k.mCode; 417 mPrefDistance = dist; 418 if (DEBUG_PREFERRED_LETTER) { 419 Log.d(TAG, "CORRECTED ALTHOUGH PREFERRED !!!!!!"); 420 } 421 break; 422 } 423 } 424 } 425 426 return mPrefLetter == code; 427 } 428 } 429 430 // Get the surrounding keys and intersect with the preferred list 431 // For all in the intersection 432 // if distance from touch point is within a reasonable distance 433 // make this the pref letter 434 // If no pref letter 435 // return inside; 436 // else return thiskey == prefletter; 437 438 for (int i = 0; i < nearby.length; i++) { 439 Key k = nearbyKeys.get(nearby[i]); 440 if (inPrefList(k.mCode, pref)) { 441 final int dist = distanceFrom(k, x, y); 442 if (dist < (int) (k.mWidth * OVERLAP_PERCENTAGE_HIGH_PROB) 443 && dist < mPrefDistance) { 444 mPrefLetter = k.mCode; 445 mPrefLetterX = x; 446 mPrefLetterY = y; 447 mPrefDistance = dist; 448 } 449 } 450 } 451 // Didn't find any 452 if (mPrefLetter == 0) { 453 return isOnKey; 454 } else { 455 return mPrefLetter == code; 456 } 457 } 458 } 459 460 // Lock into the spacebar 461 if (mCurrentlyInSpace) return false; 462 463 return key.isOnKey(x, y); 464 } 465 466 private boolean inPrefList(int code, int[] pref) { 467 if (code < pref.length && code >= 0) return pref[code] > 0; 468 return false; 469 } 470 471 private int distanceFrom(Key k, int x, int y) { 472 if (y > k.mY && y < k.mY + k.mHeight) { 473 return Math.abs(k.mX + k.mWidth / 2 - x); 474 } else { 475 return Integer.MAX_VALUE; 476 } 477 } 478 479 @Override 480 public int[] getNearestKeys(int x, int y) { 481 if (mCurrentlyInSpace) { 482 return mSpaceKeyIndexArray; 483 } else { 484 // Avoid dead pixels at edges of the keyboard 485 return super.getNearestKeys(Math.max(0, Math.min(x, getMinWidth() - 1)), 486 Math.max(0, Math.min(y, getHeight() - 1))); 487 } 488 } 489 490 private int getTextSizeFromTheme(int style, int defValue) { 491 TypedArray array = mContext.getTheme().obtainStyledAttributes( 492 style, new int[] { android.R.attr.textSize }); 493 int textSize = array.getDimensionPixelSize(array.getResourceId(0, 0), defValue); 494 return textSize; 495 } 496} 497