1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.keyboard; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Align; 27 import android.graphics.PorterDuff; 28 import android.graphics.Rect; 29 import android.graphics.Region.Op; 30 import android.graphics.Typeface; 31 import android.graphics.drawable.Drawable; 32 import android.os.Message; 33 import android.util.AttributeSet; 34 import android.util.TypedValue; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.RelativeLayout; 39 import android.widget.TextView; 40 41 import com.android.inputmethod.compat.FrameLayoutCompatUtils; 42 import com.android.inputmethod.latin.LatinImeLogger; 43 import com.android.inputmethod.latin.R; 44 import com.android.inputmethod.latin.StaticInnerHandlerWrapper; 45 46 import java.util.HashMap; 47 48 /** 49 * A view that renders a virtual {@link Keyboard}. 50 * 51 * @attr ref R.styleable#KeyboardView_backgroundDimAmount 52 * @attr ref R.styleable#KeyboardView_keyBackground 53 * @attr ref R.styleable#KeyboardView_keyLetterRatio 54 * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio 55 * @attr ref R.styleable#KeyboardView_keyLabelRatio 56 * @attr ref R.styleable#KeyboardView_keyHintLetterRatio 57 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio 58 * @attr ref R.styleable#KeyboardView_keyHintLabelRatio 59 * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding 60 * @attr ref R.styleable#KeyboardView_keyHintLetterPadding 61 * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding 62 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterPadding 63 * @attr ref R.styleable#KeyboardView_keyTextStyle 64 * @attr ref R.styleable#KeyboardView_keyPreviewLayout 65 * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio 66 * @attr ref R.styleable#KeyboardView_keyPreviewOffset 67 * @attr ref R.styleable#KeyboardView_keyPreviewHeight 68 * @attr ref R.styleable#KeyboardView_keyTextColor 69 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled 70 * @attr ref R.styleable#KeyboardView_keyHintLetterColor 71 * @attr ref R.styleable#KeyboardView_keyHintLabelColor 72 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor 73 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor 74 * @attr ref R.styleable#KeyboardView_shadowColor 75 * @attr ref R.styleable#KeyboardView_shadowRadius 76 */ 77 public class KeyboardView extends View implements PointerTracker.DrawingProxy { 78 // Miscellaneous constants 79 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 80 81 // XML attributes 82 protected final float mVerticalCorrection; 83 protected final int mMoreKeysLayout; 84 private final float mBackgroundDimAmount; 85 86 // HORIZONTAL ELLIPSIS "...", character for popup hint. 87 private static final String POPUP_HINT_CHAR = "\u2026"; 88 89 // Margin between the label and the icon on a key that has both of them. 90 // Specified by the fraction of the key width. 91 // TODO: Use resource parameter for this value. 92 private static final float LABEL_ICON_MARGIN = 0.05f; 93 94 // The maximum key label width in the proportion to the key width. 95 private static final float MAX_LABEL_RATIO = 0.90f; 96 97 // Main keyboard 98 private Keyboard mKeyboard; 99 private final KeyDrawParams mKeyDrawParams; 100 101 // Key preview 102 private final int mKeyPreviewLayoutId; 103 protected final KeyPreviewDrawParams mKeyPreviewDrawParams; 104 private boolean mShowKeyPreviewPopup = true; 105 private final int mDelayBeforePreview; 106 private int mDelayAfterPreview; 107 private ViewGroup mPreviewPlacer; 108 109 // Drawing 110 /** True if the entire keyboard needs to be dimmed. */ 111 private boolean mNeedsToDimBackground; 112 /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/ 113 private boolean mBufferNeedsUpdate; 114 /** The dirty region in the keyboard bitmap */ 115 private final Rect mDirtyRect = new Rect(); 116 /** The key to invalidate. */ 117 private Key mInvalidatedKey; 118 /** The dirty region for single key drawing */ 119 private final Rect mInvalidatedKeyRect = new Rect(); 120 /** The keyboard bitmap buffer for faster updates */ 121 private Bitmap mBuffer; 122 /** The canvas for the above mutable keyboard bitmap */ 123 private Canvas mCanvas; 124 private final Paint mPaint = new Paint(); 125 // This map caches key label text height in pixel as value and key label text size as map key. 126 private static final HashMap<Integer, Float> sTextHeightCache = 127 new HashMap<Integer, Float>(); 128 // This map caches key label text width in pixel as value and key label text size as map key. 129 private static final HashMap<Integer, Float> sTextWidthCache = 130 new HashMap<Integer, Float>(); 131 private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; 132 private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; 133 134 private final DrawingHandler mDrawingHandler = new DrawingHandler(this); 135 136 public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> { 137 private static final int MSG_SHOW_KEY_PREVIEW = 1; 138 private static final int MSG_DISMISS_KEY_PREVIEW = 2; 139 140 public DrawingHandler(KeyboardView outerInstance) { 141 super(outerInstance); 142 } 143 144 @Override 145 public void handleMessage(Message msg) { 146 final KeyboardView keyboardView = getOuterInstance(); 147 if (keyboardView == null) return; 148 final PointerTracker tracker = (PointerTracker) msg.obj; 149 switch (msg.what) { 150 case MSG_SHOW_KEY_PREVIEW: 151 keyboardView.showKey(msg.arg1, tracker); 152 break; 153 case MSG_DISMISS_KEY_PREVIEW: 154 tracker.getKeyPreviewText().setVisibility(View.INVISIBLE); 155 break; 156 } 157 } 158 159 public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) { 160 removeMessages(MSG_SHOW_KEY_PREVIEW); 161 final KeyboardView keyboardView = getOuterInstance(); 162 if (keyboardView == null) return; 163 if (tracker.getKeyPreviewText().getVisibility() == VISIBLE || delay == 0) { 164 // Show right away, if it's already visible and finger is moving around 165 keyboardView.showKey(keyIndex, tracker); 166 } else { 167 sendMessageDelayed( 168 obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay); 169 } 170 } 171 172 public void cancelShowKeyPreview(PointerTracker tracker) { 173 removeMessages(MSG_SHOW_KEY_PREVIEW, tracker); 174 } 175 176 public void cancelAllShowKeyPreviews() { 177 removeMessages(MSG_SHOW_KEY_PREVIEW); 178 } 179 180 public void dismissKeyPreview(long delay, PointerTracker tracker) { 181 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 182 } 183 184 public void cancelDismissKeyPreview(PointerTracker tracker) { 185 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 186 } 187 188 public void cancelAllDismissKeyPreviews() { 189 removeMessages(MSG_DISMISS_KEY_PREVIEW); 190 } 191 192 public void cancelAllMessages() { 193 cancelAllShowKeyPreviews(); 194 cancelAllDismissKeyPreviews(); 195 } 196 } 197 198 private static class KeyDrawParams { 199 // XML attributes 200 public final int mKeyTextColor; 201 public final int mKeyTextInactivatedColor; 202 public final Typeface mKeyTextStyle; 203 public final float mKeyLabelHorizontalPadding; 204 public final float mKeyHintLetterPadding; 205 public final float mKeyPopupHintLetterPadding; 206 public final float mKeyUppercaseLetterPadding; 207 public final int mShadowColor; 208 public final float mShadowRadius; 209 public final Drawable mKeyBackground; 210 public final int mKeyHintLetterColor; 211 public final int mKeyHintLabelColor; 212 public final int mKeyUppercaseLetterInactivatedColor; 213 public final int mKeyUppercaseLetterActivatedColor; 214 215 private final float mKeyLetterRatio; 216 private final float mKeyLargeLetterRatio; 217 private final float mKeyLabelRatio; 218 private final float mKeyHintLetterRatio; 219 private final float mKeyUppercaseLetterRatio; 220 private final float mKeyHintLabelRatio; 221 private static final float UNDEFINED_RATIO = -1.0f; 222 223 public final Rect mPadding = new Rect(); 224 public int mKeyLetterSize; 225 public int mKeyLargeLetterSize; 226 public int mKeyLabelSize; 227 public int mKeyHintLetterSize; 228 public int mKeyUppercaseLetterSize; 229 public int mKeyHintLabelSize; 230 231 public KeyDrawParams(TypedArray a) { 232 mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); 233 if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) { 234 mKeyLetterRatio = UNDEFINED_RATIO; 235 mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0); 236 } else { 237 mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio); 238 } 239 if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) { 240 mKeyLabelRatio = UNDEFINED_RATIO; 241 mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0); 242 } else { 243 mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio); 244 } 245 mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio); 246 mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio); 247 mKeyUppercaseLetterRatio = getRatio(a, 248 R.styleable.KeyboardView_keyUppercaseLetterRatio); 249 mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio); 250 mKeyLabelHorizontalPadding = a.getDimension( 251 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); 252 mKeyHintLetterPadding = a.getDimension( 253 R.styleable.KeyboardView_keyHintLetterPadding, 0); 254 mKeyPopupHintLetterPadding = a.getDimension( 255 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); 256 mKeyUppercaseLetterPadding = a.getDimension( 257 R.styleable.KeyboardView_keyUppercaseLetterPadding, 0); 258 mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000); 259 mKeyTextInactivatedColor = a.getColor( 260 R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000); 261 mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0); 262 mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0); 263 mKeyUppercaseLetterInactivatedColor = a.getColor( 264 R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0); 265 mKeyUppercaseLetterActivatedColor = a.getColor( 266 R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0); 267 mKeyTextStyle = Typeface.defaultFromStyle( 268 a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL)); 269 mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0); 270 mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f); 271 272 mKeyBackground.getPadding(mPadding); 273 } 274 275 public void updateKeyHeight(int keyHeight) { 276 if (mKeyLetterRatio >= 0.0f) 277 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 278 if (mKeyLabelRatio >= 0.0f) 279 mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio); 280 mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio); 281 mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio); 282 mKeyUppercaseLetterSize = (int)(keyHeight * mKeyUppercaseLetterRatio); 283 mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio); 284 } 285 } 286 287 protected static class KeyPreviewDrawParams { 288 // XML attributes. 289 public final Drawable mPreviewBackground; 290 public final Drawable mPreviewLeftBackground; 291 public final Drawable mPreviewRightBackground; 292 public final int mPreviewBackgroundWidth; 293 public final int mPreviewBackgroundHeight; 294 public final int mPreviewTextColor; 295 public final int mPreviewOffset; 296 public final int mPreviewHeight; 297 public final Typeface mKeyTextStyle; 298 299 private final float mPreviewTextRatio; 300 private final float mKeyLetterRatio; 301 302 public int mPreviewTextSize; 303 public int mKeyLetterSize; 304 public final int[] mCoordinates = new int[2]; 305 306 private static final int PREVIEW_ALPHA = 240; 307 308 public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) { 309 mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground); 310 mPreviewLeftBackground = a.getDrawable( 311 R.styleable.KeyboardView_keyPreviewLeftBackground); 312 mPreviewRightBackground = a.getDrawable( 313 R.styleable.KeyboardView_keyPreviewRightBackground); 314 setAlpha(mPreviewBackground, PREVIEW_ALPHA); 315 setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); 316 setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); 317 mPreviewBackgroundWidth = a.getDimensionPixelSize( 318 R.styleable.KeyboardView_keyPreviewBackgroundWidth, 0); 319 mPreviewBackgroundHeight = a.getDimensionPixelSize( 320 R.styleable.KeyboardView_keyPreviewBackgroundHeight, 0); 321 mPreviewOffset = a.getDimensionPixelOffset( 322 R.styleable.KeyboardView_keyPreviewOffset, 0); 323 mPreviewHeight = a.getDimensionPixelSize( 324 R.styleable.KeyboardView_keyPreviewHeight, 80); 325 mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio); 326 mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0); 327 328 mKeyLetterRatio = keyDrawParams.mKeyLetterRatio; 329 mKeyTextStyle = keyDrawParams.mKeyTextStyle; 330 } 331 332 public void updateKeyHeight(int keyHeight) { 333 mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); 334 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 335 } 336 337 private static void setAlpha(Drawable drawable, int alpha) { 338 if (drawable == null) 339 return; 340 drawable.setAlpha(alpha); 341 } 342 } 343 344 public KeyboardView(Context context, AttributeSet attrs) { 345 this(context, attrs, R.attr.keyboardViewStyle); 346 } 347 348 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 349 super(context, attrs, defStyle); 350 351 final TypedArray a = context.obtainStyledAttributes( 352 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 353 354 mKeyDrawParams = new KeyDrawParams(a); 355 mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams); 356 mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); 357 if (mKeyPreviewLayoutId == 0) { 358 mShowKeyPreviewPopup = false; 359 } 360 mVerticalCorrection = a.getDimensionPixelOffset( 361 R.styleable.KeyboardView_verticalCorrection, 0); 362 mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0); 363 mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f); 364 a.recycle(); 365 366 final Resources res = getResources(); 367 368 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 369 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 370 371 mPaint.setAntiAlias(true); 372 mPaint.setTextAlign(Align.CENTER); 373 mPaint.setAlpha(255); 374 } 375 376 // Read fraction value in TypedArray as float. 377 private static float getRatio(TypedArray a, int index) { 378 return a.getFraction(index, 1000, 1000, 1) / 1000.0f; 379 } 380 381 /** 382 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 383 * view will re-layout itself to accommodate the keyboard. 384 * @see Keyboard 385 * @see #getKeyboard() 386 * @param keyboard the keyboard to display in this view 387 */ 388 public void setKeyboard(Keyboard keyboard) { 389 // Remove any pending dismissing preview 390 mDrawingHandler.cancelAllShowKeyPreviews(); 391 if (mKeyboard != null) { 392 PointerTracker.dismissAllKeyPreviews(); 393 } 394 mKeyboard = keyboard; 395 LatinImeLogger.onSetKeyboard(keyboard); 396 requestLayout(); 397 mDirtyRect.set(0, 0, getWidth(), getHeight()); 398 mBufferNeedsUpdate = true; 399 invalidateAllKeys(); 400 final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; 401 mKeyDrawParams.updateKeyHeight(keyHeight); 402 mKeyPreviewDrawParams.updateKeyHeight(keyHeight); 403 } 404 405 /** 406 * Returns the current keyboard being displayed by this view. 407 * @return the currently attached keyboard 408 * @see #setKeyboard(Keyboard) 409 */ 410 public Keyboard getKeyboard() { 411 return mKeyboard; 412 } 413 414 /** 415 * Enables or disables the key feedback popup. This is a popup that shows a magnified 416 * version of the depressed key. By default the preview is enabled. 417 * @param previewEnabled whether or not to enable the key feedback preview 418 * @param delay the delay after which the preview is dismissed 419 * @see #isKeyPreviewPopupEnabled() 420 */ 421 public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { 422 mShowKeyPreviewPopup = previewEnabled; 423 mDelayAfterPreview = delay; 424 } 425 426 /** 427 * Returns the enabled state of the key feedback preview 428 * @return whether or not the key feedback preview is enabled 429 * @see #setKeyPreviewPopupEnabled(boolean, int) 430 */ 431 public boolean isKeyPreviewPopupEnabled() { 432 return mShowKeyPreviewPopup; 433 } 434 435 @Override 436 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 437 if (mKeyboard != null) { 438 // The main keyboard expands to the display width. 439 final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom(); 440 setMeasuredDimension(widthMeasureSpec, height); 441 } else { 442 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 443 } 444 } 445 446 @Override 447 public void onDraw(Canvas canvas) { 448 super.onDraw(canvas); 449 if (mBufferNeedsUpdate || mBuffer == null) { 450 mBufferNeedsUpdate = false; 451 onBufferDraw(); 452 } 453 canvas.drawBitmap(mBuffer, 0, 0, null); 454 } 455 456 private void onBufferDraw() { 457 final int width = getWidth(); 458 final int height = getHeight(); 459 if (width == 0 || height == 0) 460 return; 461 if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { 462 if (mBuffer != null) 463 mBuffer.recycle(); 464 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 465 mDirtyRect.union(0, 0, width, height); 466 if (mCanvas != null) { 467 mCanvas.setBitmap(mBuffer); 468 } else { 469 mCanvas = new Canvas(mBuffer); 470 } 471 } 472 final Canvas canvas = mCanvas; 473 canvas.clipRect(mDirtyRect, Op.REPLACE); 474 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 475 476 if (mKeyboard == null) return; 477 478 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 479 final KeyDrawParams params = mKeyDrawParams; 480 if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) { 481 // Draw a single key. 482 final int keyDrawX = mInvalidatedKey.mX + mInvalidatedKey.mVisualInsetsLeft 483 + getPaddingLeft(); 484 final int keyDrawY = mInvalidatedKey.mY + getPaddingTop(); 485 canvas.translate(keyDrawX, keyDrawY); 486 onBufferDrawKey(mInvalidatedKey, mKeyboard, canvas, mPaint, params, 487 isManualTemporaryUpperCase); 488 canvas.translate(-keyDrawX, -keyDrawY); 489 } else { 490 // Draw all keys. 491 for (final Key key : mKeyboard.mKeys) { 492 final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft(); 493 final int keyDrawY = key.mY + getPaddingTop(); 494 canvas.translate(keyDrawX, keyDrawY); 495 onBufferDrawKey(key, mKeyboard, canvas, mPaint, params, isManualTemporaryUpperCase); 496 canvas.translate(-keyDrawX, -keyDrawY); 497 } 498 } 499 500 // Overlay a dark rectangle to dim the entire keyboard 501 if (mNeedsToDimBackground) { 502 mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 503 canvas.drawRect(0, 0, width, height, mPaint); 504 } 505 506 mInvalidatedKey = null; 507 mDirtyRect.setEmpty(); 508 } 509 510 public void dimEntireKeyboard(boolean dimmed) { 511 final boolean needsRedrawing = mNeedsToDimBackground != dimmed; 512 mNeedsToDimBackground = dimmed; 513 if (needsRedrawing) { 514 invalidateAllKeys(); 515 } 516 } 517 518 private static void onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas, 519 Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase) { 520 final boolean debugShowAlign = LatinImeLogger.sVISUALDEBUG; 521 // Draw key background. 522 if (!key.isSpacer()) { 523 final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight 524 + params.mPadding.left + params.mPadding.right; 525 final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom; 526 final int bgX = -params.mPadding.left; 527 final int bgY = -params.mPadding.top; 528 final int[] drawableState = key.getCurrentDrawableState(); 529 final Drawable background = params.mKeyBackground; 530 background.setState(drawableState); 531 final Rect bounds = background.getBounds(); 532 if (bgWidth != bounds.right || bgHeight != bounds.bottom) { 533 background.setBounds(0, 0, bgWidth, bgHeight); 534 } 535 canvas.translate(bgX, bgY); 536 background.draw(canvas); 537 if (debugShowAlign) { 538 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); 539 } 540 canvas.translate(-bgX, -bgY); 541 } 542 543 // Draw key top visuals. 544 final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 545 final int keyHeight = key.mHeight; 546 final float centerX = keyWidth * 0.5f; 547 final float centerY = keyHeight * 0.5f; 548 549 if (debugShowAlign) { 550 drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); 551 } 552 553 // Draw key label. 554 final Drawable icon = key.getIcon(); 555 float positionX = centerX; 556 if (key.mLabel != null) { 557 // Switch the character to uppercase if shift is pressed 558 final CharSequence label = keyboard.adjustLabelCase(key.mLabel); 559 // For characters, use large font. For labels like "Done", use smaller font. 560 paint.setTypeface(key.selectTypeface(params.mKeyTextStyle)); 561 final int labelSize = key.selectTextSize(params.mKeyLetterSize, 562 params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyHintLabelSize); 563 paint.setTextSize(labelSize); 564 final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); 565 final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); 566 567 // Vertical label text alignment. 568 final float baseline = centerY + labelCharHeight / 2; 569 570 // Horizontal label text alignment 571 float labelWidth = 0; 572 if (key.isAlignLeft()) { 573 positionX = (int)params.mKeyLabelHorizontalPadding; 574 paint.setTextAlign(Align.LEFT); 575 } else if (key.isAlignRight()) { 576 positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding; 577 paint.setTextAlign(Align.RIGHT); 578 } else if (key.isAlignLeftOfCenter()) { 579 // TODO: Parameterise this? 580 positionX = centerX - labelCharWidth * 7 / 4; 581 paint.setTextAlign(Align.LEFT); 582 } else if (key.hasLabelWithIconLeft() && icon != null) { 583 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 584 + LABEL_ICON_MARGIN * keyWidth; 585 positionX = centerX + labelWidth / 2; 586 paint.setTextAlign(Align.RIGHT); 587 } else if (key.hasLabelWithIconRight() && icon != null) { 588 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 589 + LABEL_ICON_MARGIN * keyWidth; 590 positionX = centerX - labelWidth / 2; 591 paint.setTextAlign(Align.LEFT); 592 } else { 593 positionX = centerX; 594 paint.setTextAlign(Align.CENTER); 595 } 596 if (key.needsXScale()) { 597 paint.setTextScaleX( 598 Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); 599 } 600 601 if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) { 602 paint.setColor(params.mKeyTextInactivatedColor); 603 } else { 604 paint.setColor(params.mKeyTextColor); 605 } 606 if (key.isEnabled()) { 607 // Set a drop shadow for the text 608 paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor); 609 } else { 610 // Make label invisible 611 paint.setColor(Color.TRANSPARENT); 612 } 613 canvas.drawText(label, 0, label.length(), positionX, baseline, paint); 614 // Turn off drop shadow and reset x-scale. 615 paint.setShadowLayer(0, 0, 0, 0); 616 paint.setTextScaleX(1.0f); 617 618 if (icon != null) { 619 final int iconWidth = icon.getIntrinsicWidth(); 620 final int iconHeight = icon.getIntrinsicHeight(); 621 final int iconY = (keyHeight - iconHeight) / 2; 622 if (key.hasLabelWithIconLeft()) { 623 final int iconX = (int)(centerX - labelWidth / 2); 624 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 625 } else if (key.hasLabelWithIconRight()) { 626 final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); 627 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 628 } 629 } 630 631 if (debugShowAlign) { 632 final Paint line = new Paint(); 633 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line); 634 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line); 635 } 636 } 637 638 // Draw hint label. 639 if (key.mHintLabel != null) { 640 final CharSequence hint = key.mHintLabel; 641 final int hintColor; 642 final int hintSize; 643 if (key.hasHintLabel()) { 644 hintColor = params.mKeyHintLabelColor; 645 hintSize = params.mKeyHintLabelSize; 646 paint.setTypeface(Typeface.DEFAULT); 647 } else if (key.hasUppercaseLetter()) { 648 hintColor = isManualTemporaryUpperCase 649 ? params.mKeyUppercaseLetterActivatedColor 650 : params.mKeyUppercaseLetterInactivatedColor; 651 hintSize = params.mKeyUppercaseLetterSize; 652 } else { // key.hasHintLetter() 653 hintColor = params.mKeyHintLetterColor; 654 hintSize = params.mKeyHintLetterSize; 655 } 656 paint.setColor(hintColor); 657 paint.setTextSize(hintSize); 658 final float hintX, hintY; 659 if (key.hasHintLabel()) { 660 // The hint label is placed just right of the key label. Used mainly on 661 // "phone number" layout. 662 // TODO: Generalize the following calculations. 663 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; 664 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 665 paint.setTextAlign(Align.LEFT); 666 } else if (key.hasUppercaseLetter()) { 667 // The hint label is placed at top-right corner of the key. Used mainly on tablet. 668 hintX = keyWidth - params.mKeyUppercaseLetterPadding 669 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 670 hintY = -paint.ascent(); 671 paint.setTextAlign(Align.CENTER); 672 } else { // key.hasHintLetter() 673 // The hint label is placed at top-right corner of the key. Used mainly on phone. 674 hintX = keyWidth - params.mKeyHintLetterPadding 675 - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; 676 hintY = -paint.ascent(); 677 paint.setTextAlign(Align.CENTER); 678 } 679 canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint); 680 681 if (debugShowAlign) { 682 final Paint line = new Paint(); 683 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 684 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 685 } 686 } 687 688 // Draw key icon. 689 if (key.mLabel == null && icon != null) { 690 final int iconWidth = icon.getIntrinsicWidth(); 691 final int iconHeight = icon.getIntrinsicHeight(); 692 final int iconX, alignX; 693 final int iconY = (keyHeight - iconHeight) / 2; 694 if (key.isAlignLeft()) { 695 iconX = (int)params.mKeyLabelHorizontalPadding; 696 alignX = iconX; 697 } else if (key.isAlignRight()) { 698 iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth; 699 alignX = iconX + iconWidth; 700 } else { // Align center 701 iconX = (keyWidth - iconWidth) / 2; 702 alignX = iconX + iconWidth / 2; 703 } 704 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 705 706 if (debugShowAlign) { 707 final Paint line = new Paint(); 708 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line); 709 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); 710 } 711 } 712 713 // Draw popup hint "..." at the bottom right corner of the key. 714 if ((key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) 715 || key.needsSpecialPopupHint()) { 716 paint.setTextSize(params.mKeyHintLetterSize); 717 paint.setColor(params.mKeyHintLabelColor); 718 paint.setTextAlign(Align.CENTER); 719 final float hintX = keyWidth - params.mKeyHintLetterPadding 720 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 721 final float hintY = keyHeight - params.mKeyPopupHintLetterPadding; 722 canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); 723 724 if (debugShowAlign) { 725 final Paint line = new Paint(); 726 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 727 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 728 } 729 } 730 } 731 732 private static final Rect sTextBounds = new Rect(); 733 734 private static int getCharGeometryCacheKey(char reference, Paint paint) { 735 final int labelSize = (int)paint.getTextSize(); 736 final Typeface face = paint.getTypeface(); 737 final int codePointOffset = reference << 15; 738 if (face == Typeface.DEFAULT) { 739 return codePointOffset + labelSize; 740 } else if (face == Typeface.DEFAULT_BOLD) { 741 return codePointOffset + labelSize + 0x1000; 742 } else if (face == Typeface.MONOSPACE) { 743 return codePointOffset + labelSize + 0x2000; 744 } else { 745 return codePointOffset + labelSize; 746 } 747 } 748 749 private static float getCharHeight(char[] character, Paint paint) { 750 final Integer key = getCharGeometryCacheKey(character[0], paint); 751 final Float cachedValue = sTextHeightCache.get(key); 752 if (cachedValue != null) 753 return cachedValue; 754 755 paint.getTextBounds(character, 0, 1, sTextBounds); 756 final float height = sTextBounds.height(); 757 sTextHeightCache.put(key, height); 758 return height; 759 } 760 761 private static float getCharWidth(char[] character, Paint paint) { 762 final Integer key = getCharGeometryCacheKey(character[0], paint); 763 final Float cachedValue = sTextWidthCache.get(key); 764 if (cachedValue != null) 765 return cachedValue; 766 767 paint.getTextBounds(character, 0, 1, sTextBounds); 768 final float width = sTextBounds.width(); 769 sTextWidthCache.put(key, width); 770 return width; 771 } 772 773 private static float getLabelWidth(CharSequence label, Paint paint) { 774 paint.getTextBounds(label.toString(), 0, label.length(), sTextBounds); 775 return sTextBounds.width(); 776 } 777 778 public float getDefaultLabelWidth(CharSequence label, Paint paint) { 779 paint.setTextSize(mKeyDrawParams.mKeyLabelSize); 780 paint.setTypeface(mKeyDrawParams.mKeyTextStyle); 781 return getLabelWidth(label, paint); 782 } 783 784 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 785 int height) { 786 canvas.translate(x, y); 787 icon.setBounds(0, 0, width, height); 788 icon.draw(canvas); 789 canvas.translate(-x, -y); 790 } 791 792 private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, 793 Paint paint) { 794 paint.setStyle(Paint.Style.STROKE); 795 paint.setStrokeWidth(1.0f); 796 paint.setColor(color); 797 canvas.drawLine(0, y, w, y, paint); 798 } 799 800 private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) { 801 paint.setStyle(Paint.Style.STROKE); 802 paint.setStrokeWidth(1.0f); 803 paint.setColor(color); 804 canvas.drawLine(x, 0, x, h, paint); 805 } 806 807 private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, 808 Paint paint) { 809 paint.setStyle(Paint.Style.STROKE); 810 paint.setStrokeWidth(1.0f); 811 paint.setColor(color); 812 canvas.translate(x, y); 813 canvas.drawRect(0, 0, w, h, paint); 814 canvas.translate(-x, -y); 815 } 816 817 public void cancelAllMessages() { 818 mDrawingHandler.cancelAllMessages(); 819 } 820 821 // Called by {@link PointerTracker} constructor to create a TextView. 822 @Override 823 public TextView inflateKeyPreviewText() { 824 final Context context = getContext(); 825 if (mKeyPreviewLayoutId != 0) { 826 return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); 827 } else { 828 return new TextView(context); 829 } 830 } 831 832 @Override 833 public void showKeyPreview(int keyIndex, PointerTracker tracker) { 834 if (mShowKeyPreviewPopup) { 835 mDrawingHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker); 836 } 837 } 838 839 @Override 840 public void cancelShowKeyPreview(PointerTracker tracker) { 841 mDrawingHandler.cancelShowKeyPreview(tracker); 842 } 843 844 @Override 845 public void dismissKeyPreview(PointerTracker tracker) { 846 mDrawingHandler.cancelShowKeyPreview(tracker); 847 mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 848 } 849 850 private void addKeyPreview(TextView keyPreview) { 851 if (mPreviewPlacer == null) { 852 mPreviewPlacer = new RelativeLayout(getContext()); 853 final ViewGroup windowContentView = 854 (ViewGroup)getRootView().findViewById(android.R.id.content); 855 windowContentView.addView(mPreviewPlacer); 856 } 857 mPreviewPlacer.addView( 858 keyPreview, FrameLayoutCompatUtils.newLayoutParam(mPreviewPlacer, 0, 0)); 859 } 860 861 private void showKey(final int keyIndex, PointerTracker tracker) { 862 final TextView previewText = tracker.getKeyPreviewText(); 863 // If the key preview has no parent view yet, add it to the ViewGroup which can place 864 // key preview absolutely in SoftInputWindow. 865 if (previewText.getParent() == null) { 866 addKeyPreview(previewText); 867 } 868 869 mDrawingHandler.cancelDismissKeyPreview(tracker); 870 final Key key = tracker.getKey(keyIndex); 871 // If keyIndex is invalid or IME is already closed, we must not show key preview. 872 // Trying to show key preview while root window is closed causes 873 // WindowManager.BadTokenException. 874 if (key == null) 875 return; 876 877 final KeyPreviewDrawParams params = mKeyPreviewDrawParams; 878 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 879 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 880 // What we show as preview should match what we show on key top in onBufferDraw(). 881 if (key.mLabel != null) { 882 // TODO Should take care of temporaryShiftLabel here. 883 previewText.setCompoundDrawables(null, null, null, null); 884 if (key.mLabel.length() > 1) { 885 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize); 886 previewText.setTypeface(Typeface.DEFAULT_BOLD); 887 } else { 888 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize); 889 previewText.setTypeface(params.mKeyTextStyle); 890 } 891 previewText.setText(mKeyboard.adjustLabelCase(key.mLabel)); 892 } else { 893 final Drawable previewIcon = key.getPreviewIcon(); 894 previewText.setCompoundDrawables(null, null, null, 895 previewIcon != null ? previewIcon : key.getIcon()); 896 previewText.setText(null); 897 } 898 previewText.setBackgroundDrawable(params.mPreviewBackground); 899 900 previewText.measure( 901 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 902 final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth 903 + previewText.getPaddingLeft() + previewText.getPaddingRight()); 904 final int previewHeight = params.mPreviewHeight; 905 getLocationInWindow(params.mCoordinates); 906 int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + params.mCoordinates[0]; 907 final int previewY = key.mY - previewHeight 908 + params.mCoordinates[1] + params.mPreviewOffset; 909 if (previewX < 0 && params.mPreviewLeftBackground != null) { 910 previewText.setBackgroundDrawable(params.mPreviewLeftBackground); 911 previewX = 0; 912 } else if (previewX + previewWidth > getWidth() && params.mPreviewRightBackground != null) { 913 previewText.setBackgroundDrawable(params.mPreviewRightBackground); 914 previewX = getWidth() - previewWidth; 915 } 916 917 // Set the preview background state 918 previewText.getBackground().setState( 919 key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 920 previewText.setTextColor(params.mPreviewTextColor); 921 FrameLayoutCompatUtils.placeViewAt( 922 previewText, previewX, previewY, previewWidth, previewHeight); 923 previewText.setVisibility(VISIBLE); 924 } 925 926 /** 927 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 928 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 929 * draws the cached buffer. 930 * @see #invalidateKey(Key) 931 */ 932 public void invalidateAllKeys() { 933 mDirtyRect.union(0, 0, getWidth(), getHeight()); 934 mBufferNeedsUpdate = true; 935 invalidate(); 936 } 937 938 /** 939 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 940 * one key is changing it's content. Any changes that affect the position or size of the key 941 * may not be honored. 942 * @param key key in the attached {@link Keyboard}. 943 * @see #invalidateAllKeys 944 */ 945 @Override 946 public void invalidateKey(Key key) { 947 if (key == null) 948 return; 949 mInvalidatedKey = key; 950 final int x = key.mX + getPaddingLeft(); 951 final int y = key.mY + getPaddingTop(); 952 mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight); 953 mDirtyRect.union(mInvalidatedKeyRect); 954 mBufferNeedsUpdate = true; 955 invalidate(mInvalidatedKeyRect); 956 } 957 958 public void closing() { 959 PointerTracker.dismissAllKeyPreviews(); 960 cancelAllMessages(); 961 962 mDirtyRect.union(0, 0, getWidth(), getHeight()); 963 requestLayout(); 964 } 965 966 @Override 967 public boolean dismissMoreKeysPanel() { 968 return false; 969 } 970 971 public void purgeKeyboardAndClosing() { 972 mKeyboard = null; 973 closing(); 974 } 975 976 @Override 977 protected void onDetachedFromWindow() { 978 super.onDetachedFromWindow(); 979 closing(); 980 if (mPreviewPlacer != null) { 981 mPreviewPlacer.removeAllViews(); 982 } 983 if (mBuffer != null) { 984 mBuffer.recycle(); 985 mBuffer = null; 986 } 987 } 988 } 989