KeyboardView.java revision 71b52a84e2d078d0f57712dba88f8cc691412672
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 17package com.android.inputmethod.keyboard; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.graphics.Paint.Align; 26import android.graphics.PorterDuff; 27import android.graphics.Rect; 28import android.graphics.Region; 29import android.graphics.Typeface; 30import android.graphics.drawable.Drawable; 31import android.os.Message; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.util.SparseArray; 35import android.util.TypedValue; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.widget.TextView; 40 41import com.android.inputmethod.keyboard.internal.KeyDrawParams; 42import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams; 43import com.android.inputmethod.keyboard.internal.PreviewPlacerView; 44import com.android.inputmethod.latin.CollectionUtils; 45import com.android.inputmethod.latin.Constants; 46import com.android.inputmethod.latin.LatinImeLogger; 47import com.android.inputmethod.latin.R; 48import com.android.inputmethod.latin.StaticInnerHandlerWrapper; 49import com.android.inputmethod.latin.StringUtils; 50import com.android.inputmethod.latin.define.ProductionFlag; 51import com.android.inputmethod.research.ResearchLogger; 52 53import java.util.HashSet; 54 55/** 56 * A view that renders a virtual {@link Keyboard}. 57 * 58 * @attr ref R.styleable#KeyboardView_keyBackground 59 * @attr ref R.styleable#KeyboardView_moreKeysLayout 60 * @attr ref R.styleable#KeyboardView_keyPreviewLayout 61 * @attr ref R.styleable#KeyboardView_keyPreviewBackground 62 * @attr ref R.styleable#KeyboardView_keyPreviewLeftBackground 63 * @attr ref R.styleable#KeyboardView_keyPreviewRightBackground 64 * @attr ref R.styleable#KeyboardView_keyPreviewOffset 65 * @attr ref R.styleable#KeyboardView_keyPreviewHeight 66 * @attr ref R.styleable#KeyboardView_keyPreviewLingerTimeout 67 * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding 68 * @attr ref R.styleable#KeyboardView_keyHintLetterPadding 69 * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding 70 * @attr ref R.styleable#KeyboardView_keyShiftedLetterHintPadding 71 * @attr ref R.styleable#KeyboardView_backgroundDimAlpha 72 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextSize 73 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextColor 74 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextOffset 75 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadingColor 76 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadingBorder 77 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowColor 78 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowBorder 79 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorColor 80 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorWidth 81 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout 82 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay 83 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration 84 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval 85 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor 86 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth 87 * @attr ref R.styleable#KeyboardView_verticalCorrection 88 * @attr ref R.styleable#Keyboard_Key_keyTypeface 89 * @attr ref R.styleable#Keyboard_Key_keyLetterSize 90 * @attr ref R.styleable#Keyboard_Key_keyLabelSize 91 * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio 92 * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio 93 * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio 94 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio 95 * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio 96 * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio 97 * @attr ref R.styleable#Keyboard_Key_keyTextColor 98 * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled 99 * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor 100 * @attr ref R.styleable#Keyboard_Key_keyTextShadowRadius 101 * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor 102 * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor 103 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor 104 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor 105 * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor 106 */ 107public class KeyboardView extends View implements PointerTracker.DrawingProxy { 108 private static final String TAG = KeyboardView.class.getSimpleName(); 109 110 // Miscellaneous constants 111 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 112 113 // XML attributes 114 private final int mKeyLabelHorizontalPadding; 115 private final float mKeyHintLetterPadding; 116 private final float mKeyPopupHintLetterPadding; 117 private final float mKeyShiftedLetterHintPadding; 118 protected final float mVerticalCorrection; 119 protected final int mMoreKeysLayout; 120 private final int mBackgroundDimAlpha; 121 122 // HORIZONTAL ELLIPSIS "...", character for popup hint. 123 private static final String POPUP_HINT_CHAR = "\u2026"; 124 125 // Margin between the label and the icon on a key that has both of them. 126 // Specified by the fraction of the key width. 127 // TODO: Use resource parameter for this value. 128 private static final float LABEL_ICON_MARGIN = 0.05f; 129 130 // The maximum key label width in the proportion to the key width. 131 private static final float MAX_LABEL_RATIO = 0.90f; 132 133 // Main keyboard 134 private Keyboard mKeyboard; 135 protected final KeyDrawParams mKeyDrawParams; 136 137 // Key preview 138 private final int mKeyPreviewLayoutId; 139 private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray(); 140 protected final KeyPreviewDrawParams mKeyPreviewDrawParams; 141 private boolean mShowKeyPreviewPopup = true; 142 private int mDelayAfterPreview; 143 private final PreviewPlacerView mPreviewPlacerView; 144 145 // Drawing 146 /** True if the entire keyboard needs to be dimmed. */ 147 private boolean mNeedsToDimEntireKeyboard; 148 /** True if all keys should be drawn */ 149 private boolean mInvalidateAllKeys; 150 /** The keys that should be drawn */ 151 private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet(); 152 /** The working rectangle variable */ 153 private final Rect mWorkingRect = new Rect(); 154 /** The keyboard bitmap buffer for faster updates */ 155 /** The clip region to draw keys */ 156 private final Region mClipRegion = new Region(); 157 private Bitmap mOffscreenBuffer; 158 /** The canvas for the above mutable keyboard bitmap */ 159 private Canvas mOffscreenCanvas; 160 private final Paint mPaint = new Paint(); 161 private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); 162 // This sparse array caches key label text height in pixel indexed by key label text size. 163 private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); 164 // This sparse array caches key label text width in pixel indexed by key label text size. 165 private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); 166 private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; 167 private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; 168 169 private final DrawingHandler mDrawingHandler = new DrawingHandler(this); 170 171 public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> { 172 private static final int MSG_DISMISS_KEY_PREVIEW = 0; 173 174 public DrawingHandler(KeyboardView outerInstance) { 175 super(outerInstance); 176 } 177 178 @Override 179 public void handleMessage(Message msg) { 180 final KeyboardView keyboardView = getOuterInstance(); 181 if (keyboardView == null) return; 182 final PointerTracker tracker = (PointerTracker) msg.obj; 183 switch (msg.what) { 184 case MSG_DISMISS_KEY_PREVIEW: 185 final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId); 186 if (previewText != null) { 187 previewText.setVisibility(INVISIBLE); 188 } 189 break; 190 } 191 } 192 193 public void dismissKeyPreview(long delay, PointerTracker tracker) { 194 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 195 } 196 197 public void cancelDismissKeyPreview(PointerTracker tracker) { 198 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 199 } 200 201 private void cancelAllDismissKeyPreviews() { 202 removeMessages(MSG_DISMISS_KEY_PREVIEW); 203 } 204 205 public void cancelAllMessages() { 206 cancelAllDismissKeyPreviews(); 207 } 208 } 209 210 public KeyboardView(Context context, AttributeSet attrs) { 211 this(context, attrs, R.attr.keyboardViewStyle); 212 } 213 214 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 215 super(context, attrs, defStyle); 216 217 final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, 218 R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 219 final TypedArray keyAttr = context.obtainStyledAttributes(attrs, 220 R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView); 221 mKeyDrawParams = new KeyDrawParams(keyboardViewAttr, keyAttr); 222 mKeyPreviewDrawParams = new KeyPreviewDrawParams(keyboardViewAttr, keyAttr); 223 mDelayAfterPreview = mKeyPreviewDrawParams.mLingerTimeout; 224 mKeyLabelHorizontalPadding = keyAttr.getDimensionPixelOffset( 225 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); 226 mKeyHintLetterPadding = keyAttr.getDimension( 227 R.styleable.KeyboardView_keyHintLetterPadding, 0); 228 mKeyPopupHintLetterPadding = keyAttr.getDimension( 229 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); 230 mKeyShiftedLetterHintPadding = keyAttr.getDimension( 231 R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); 232 mKeyPreviewLayoutId = keyboardViewAttr.getResourceId( 233 R.styleable.KeyboardView_keyPreviewLayout, 0); 234 if (mKeyPreviewLayoutId == 0) { 235 mShowKeyPreviewPopup = false; 236 } 237 mVerticalCorrection = keyboardViewAttr.getDimensionPixelOffset( 238 R.styleable.KeyboardView_verticalCorrection, 0); 239 mMoreKeysLayout = keyboardViewAttr.getResourceId( 240 R.styleable.KeyboardView_moreKeysLayout, 0); 241 mBackgroundDimAlpha = keyboardViewAttr.getInt( 242 R.styleable.KeyboardView_backgroundDimAlpha, 0); 243 keyboardViewAttr.recycle(); 244 keyAttr.recycle(); 245 246 mPreviewPlacerView = new PreviewPlacerView(context, attrs); 247 mPaint.setAntiAlias(true); 248 } 249 250 /** 251 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 252 * view will re-layout itself to accommodate the keyboard. 253 * @see Keyboard 254 * @see #getKeyboard() 255 * @param keyboard the keyboard to display in this view 256 */ 257 public void setKeyboard(Keyboard keyboard) { 258 mKeyboard = keyboard; 259 LatinImeLogger.onSetKeyboard(keyboard); 260 requestLayout(); 261 invalidateAllKeys(); 262 mKeyDrawParams.updateParams(keyboard); 263 mKeyPreviewDrawParams.updateParams(keyboard, mKeyDrawParams); 264 } 265 266 /** 267 * Returns the current keyboard being displayed by this view. 268 * @return the currently attached keyboard 269 * @see #setKeyboard(Keyboard) 270 */ 271 public Keyboard getKeyboard() { 272 return mKeyboard; 273 } 274 275 /** 276 * Enables or disables the key feedback popup. This is a popup that shows a magnified 277 * version of the depressed key. By default the preview is enabled. 278 * @param previewEnabled whether or not to enable the key feedback preview 279 * @param delay the delay after which the preview is dismissed 280 * @see #isKeyPreviewPopupEnabled() 281 */ 282 public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { 283 mShowKeyPreviewPopup = previewEnabled; 284 mDelayAfterPreview = delay; 285 } 286 287 /** 288 * Returns the enabled state of the key feedback preview 289 * @return whether or not the key feedback preview is enabled 290 * @see #setKeyPreviewPopupEnabled(boolean, int) 291 */ 292 public boolean isKeyPreviewPopupEnabled() { 293 return mShowKeyPreviewPopup; 294 } 295 296 public void setGesturePreviewMode(boolean drawsGesturePreviewTrail, 297 boolean drawsGestureFloatingPreviewText) { 298 mPreviewPlacerView.setGesturePreviewMode( 299 drawsGesturePreviewTrail, drawsGestureFloatingPreviewText); 300 } 301 302 @Override 303 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 304 if (mKeyboard != null) { 305 // The main keyboard expands to the display width. 306 final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom(); 307 setMeasuredDimension(widthMeasureSpec, height); 308 } else { 309 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 310 } 311 } 312 313 @Override 314 public void onDraw(Canvas canvas) { 315 super.onDraw(canvas); 316 if (canvas.isHardwareAccelerated()) { 317 onDrawKeyboard(canvas); 318 return; 319 } 320 321 final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty(); 322 if (bufferNeedsUpdates || mOffscreenBuffer == null) { 323 if (maybeAllocateOffscreenBuffer()) { 324 mInvalidateAllKeys = true; 325 maybeCreateOffscreenCanvas(); 326 } 327 onDrawKeyboard(mOffscreenCanvas); 328 } 329 canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); 330 } 331 332 private boolean maybeAllocateOffscreenBuffer() { 333 final int width = getWidth(); 334 final int height = getHeight(); 335 if (width == 0 || height == 0) { 336 return false; 337 } 338 if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width 339 && mOffscreenBuffer.getHeight() == height) { 340 return false; 341 } 342 freeOffscreenBuffer(); 343 mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 344 return true; 345 } 346 347 private void freeOffscreenBuffer() { 348 if (mOffscreenBuffer != null) { 349 mOffscreenBuffer.recycle(); 350 mOffscreenBuffer = null; 351 } 352 } 353 354 private void maybeCreateOffscreenCanvas() { 355 // TODO: Stop using the offscreen canvas even when in software rendering 356 if (mOffscreenCanvas != null) { 357 mOffscreenCanvas.setBitmap(mOffscreenBuffer); 358 } else { 359 mOffscreenCanvas = new Canvas(mOffscreenBuffer); 360 } 361 } 362 363 private void onDrawKeyboard(final Canvas canvas) { 364 if (mKeyboard == null) return; 365 366 final int width = getWidth(); 367 final int height = getHeight(); 368 final Paint paint = mPaint; 369 final KeyDrawParams params = mKeyDrawParams; 370 371 // Calculate clip region and set. 372 final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty(); 373 final boolean isHardwareAccelerated = canvas.isHardwareAccelerated(); 374 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 375 if (drawAllKeys || isHardwareAccelerated) { 376 mClipRegion.set(0, 0, width, height); 377 } else { 378 mClipRegion.setEmpty(); 379 for (final Key key : mInvalidatedKeys) { 380 if (mKeyboard.hasKey(key)) { 381 final int x = key.mX + getPaddingLeft(); 382 final int y = key.mY + getPaddingTop(); 383 mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight); 384 mClipRegion.union(mWorkingRect); 385 } 386 } 387 } 388 if (!isHardwareAccelerated) { 389 canvas.clipRegion(mClipRegion, Region.Op.REPLACE); 390 // Draw keyboard background. 391 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 392 final Drawable background = getBackground(); 393 if (background != null) { 394 background.draw(canvas); 395 } 396 } 397 398 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 399 if (drawAllKeys || isHardwareAccelerated) { 400 // Draw all keys. 401 for (final Key key : mKeyboard.mKeys) { 402 onDrawKey(key, canvas, paint, params); 403 } 404 } else { 405 // Draw invalidated keys. 406 for (final Key key : mInvalidatedKeys) { 407 if (mKeyboard.hasKey(key)) { 408 onDrawKey(key, canvas, paint, params); 409 } 410 } 411 } 412 413 // Overlay a dark rectangle to dim. 414 if (mNeedsToDimEntireKeyboard) { 415 paint.setColor(Color.BLACK); 416 paint.setAlpha(mBackgroundDimAlpha); 417 // Note: clipRegion() above is in effect if it was called. 418 canvas.drawRect(0, 0, width, height, paint); 419 } 420 421 // ResearchLogging indicator. 422 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger, 423 // and remove this call. 424 if (ProductionFlag.IS_EXPERIMENTAL) { 425 ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height); 426 } 427 428 mInvalidatedKeys.clear(); 429 mInvalidateAllKeys = false; 430 } 431 432 public void dimEntireKeyboard(boolean dimmed) { 433 final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; 434 mNeedsToDimEntireKeyboard = dimmed; 435 if (needsRedrawing) { 436 invalidateAllKeys(); 437 } 438 } 439 440 private void onDrawKey(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { 441 final int keyDrawX = key.getDrawX() + getPaddingLeft(); 442 final int keyDrawY = key.mY + getPaddingTop(); 443 canvas.translate(keyDrawX, keyDrawY); 444 445 params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; 446 if (!key.isSpacer()) { 447 onDrawKeyBackground(key, canvas, params); 448 } 449 onDrawKeyTopVisuals(key, canvas, paint, params); 450 451 canvas.translate(-keyDrawX, -keyDrawY); 452 } 453 454 // Draw key background. 455 protected void onDrawKeyBackground(Key key, Canvas canvas, KeyDrawParams params) { 456 final int bgWidth = key.getDrawWidth() + params.mPadding.left + params.mPadding.right; 457 final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom; 458 final int bgX = -params.mPadding.left; 459 final int bgY = -params.mPadding.top; 460 final int[] drawableState = key.getCurrentDrawableState(); 461 final Drawable background = params.mKeyBackground; 462 background.setState(drawableState); 463 final Rect bounds = background.getBounds(); 464 if (bgWidth != bounds.right || bgHeight != bounds.bottom) { 465 background.setBounds(0, 0, bgWidth, bgHeight); 466 } 467 canvas.translate(bgX, bgY); 468 background.draw(canvas); 469 if (LatinImeLogger.sVISUALDEBUG) { 470 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); 471 } 472 canvas.translate(-bgX, -bgY); 473 } 474 475 // Draw key top visuals. 476 protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { 477 final int keyWidth = key.getDrawWidth(); 478 final int keyHeight = key.mHeight; 479 final float centerX = keyWidth * 0.5f; 480 final float centerY = keyHeight * 0.5f; 481 482 if (LatinImeLogger.sVISUALDEBUG) { 483 drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); 484 } 485 486 // Draw key label. 487 final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha); 488 float positionX = centerX; 489 if (key.mLabel != null) { 490 final String label = key.mLabel; 491 // For characters, use large font. For labels like "Done", use smaller font. 492 paint.setTypeface(key.selectTypeface(params.mKeyTypeface)); 493 final int labelSize = key.selectTextSize(params.mKeyLetterSize, 494 params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyLargeLabelSize, 495 params.mKeyHintLabelSize); 496 paint.setTextSize(labelSize); 497 final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); 498 final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); 499 500 // Vertical label text alignment. 501 final float baseline = centerY + labelCharHeight / 2; 502 503 // Horizontal label text alignment 504 float labelWidth = 0; 505 if (key.isAlignLeft()) { 506 positionX = mKeyLabelHorizontalPadding; 507 paint.setTextAlign(Align.LEFT); 508 } else if (key.isAlignRight()) { 509 positionX = keyWidth - mKeyLabelHorizontalPadding; 510 paint.setTextAlign(Align.RIGHT); 511 } else if (key.isAlignLeftOfCenter()) { 512 // TODO: Parameterise this? 513 positionX = centerX - labelCharWidth * 7 / 4; 514 paint.setTextAlign(Align.LEFT); 515 } else if (key.hasLabelWithIconLeft() && icon != null) { 516 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 517 + LABEL_ICON_MARGIN * keyWidth; 518 positionX = centerX + labelWidth / 2; 519 paint.setTextAlign(Align.RIGHT); 520 } else if (key.hasLabelWithIconRight() && icon != null) { 521 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 522 + LABEL_ICON_MARGIN * keyWidth; 523 positionX = centerX - labelWidth / 2; 524 paint.setTextAlign(Align.LEFT); 525 } else { 526 positionX = centerX; 527 paint.setTextAlign(Align.CENTER); 528 } 529 if (key.needsXScale()) { 530 paint.setTextScaleX( 531 Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); 532 } 533 534 paint.setColor(key.isShiftedLetterActivated() 535 ? params.mKeyTextInactivatedColor : params.mKeyTextColor); 536 if (key.isEnabled()) { 537 // Set a drop shadow for the text 538 paint.setShadowLayer(params.mKeyTextShadowRadius, 0, 0, params.mKeyTextShadowColor); 539 } else { 540 // Make label invisible 541 paint.setColor(Color.TRANSPARENT); 542 } 543 params.blendAlpha(paint); 544 canvas.drawText(label, 0, label.length(), positionX, baseline, paint); 545 // Turn off drop shadow and reset x-scale. 546 paint.setShadowLayer(0, 0, 0, 0); 547 paint.setTextScaleX(1.0f); 548 549 if (icon != null) { 550 final int iconWidth = icon.getIntrinsicWidth(); 551 final int iconHeight = icon.getIntrinsicHeight(); 552 final int iconY = (keyHeight - iconHeight) / 2; 553 if (key.hasLabelWithIconLeft()) { 554 final int iconX = (int)(centerX - labelWidth / 2); 555 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 556 } else if (key.hasLabelWithIconRight()) { 557 final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); 558 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 559 } 560 } 561 562 if (LatinImeLogger.sVISUALDEBUG) { 563 final Paint line = new Paint(); 564 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line); 565 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line); 566 } 567 } 568 569 // Draw hint label. 570 if (key.mHintLabel != null) { 571 final String hint = key.mHintLabel; 572 final int hintColor; 573 final int hintSize; 574 if (key.hasHintLabel()) { 575 hintColor = params.mKeyHintLabelColor; 576 hintSize = params.mKeyHintLabelSize; 577 paint.setTypeface(Typeface.DEFAULT); 578 } else if (key.hasShiftedLetterHint()) { 579 hintColor = key.isShiftedLetterActivated() 580 ? params.mKeyShiftedLetterHintActivatedColor 581 : params.mKeyShiftedLetterHintInactivatedColor; 582 hintSize = params.mKeyShiftedLetterHintSize; 583 } else { // key.hasHintLetter() 584 hintColor = params.mKeyHintLetterColor; 585 hintSize = params.mKeyHintLetterSize; 586 } 587 paint.setColor(hintColor); 588 params.blendAlpha(paint); 589 paint.setTextSize(hintSize); 590 final float hintX, hintY; 591 if (key.hasHintLabel()) { 592 // The hint label is placed just right of the key label. Used mainly on 593 // "phone number" layout. 594 // TODO: Generalize the following calculations. 595 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; 596 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 597 paint.setTextAlign(Align.LEFT); 598 } else if (key.hasShiftedLetterHint()) { 599 // The hint label is placed at top-right corner of the key. Used mainly on tablet. 600 hintX = keyWidth - mKeyShiftedLetterHintPadding 601 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 602 paint.getFontMetrics(mFontMetrics); 603 hintY = -mFontMetrics.top; 604 paint.setTextAlign(Align.CENTER); 605 } else { // key.hasHintLetter() 606 // The hint letter is placed at top-right corner of the key. Used mainly on phone. 607 hintX = keyWidth - mKeyHintLetterPadding 608 - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; 609 hintY = -paint.ascent(); 610 paint.setTextAlign(Align.CENTER); 611 } 612 canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint); 613 614 if (LatinImeLogger.sVISUALDEBUG) { 615 final Paint line = new Paint(); 616 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 617 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 618 } 619 } 620 621 // Draw key icon. 622 if (key.mLabel == null && icon != null) { 623 final int iconWidth = icon.getIntrinsicWidth(); 624 final int iconHeight = icon.getIntrinsicHeight(); 625 final int iconX, alignX; 626 final int iconY = (keyHeight - iconHeight) / 2; 627 if (key.isAlignLeft()) { 628 iconX = mKeyLabelHorizontalPadding; 629 alignX = iconX; 630 } else if (key.isAlignRight()) { 631 iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth; 632 alignX = iconX + iconWidth; 633 } else { // Align center 634 iconX = (keyWidth - iconWidth) / 2; 635 alignX = iconX + iconWidth / 2; 636 } 637 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 638 639 if (LatinImeLogger.sVISUALDEBUG) { 640 final Paint line = new Paint(); 641 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line); 642 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); 643 } 644 } 645 646 if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) { 647 drawKeyPopupHint(key, canvas, paint, params); 648 } 649 } 650 651 // Draw popup hint "..." at the bottom right corner of the key. 652 protected void drawKeyPopupHint(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { 653 final int keyWidth = key.getDrawWidth(); 654 final int keyHeight = key.mHeight; 655 656 paint.setTypeface(params.mKeyTypeface); 657 paint.setTextSize(params.mKeyHintLetterSize); 658 paint.setColor(params.mKeyHintLabelColor); 659 paint.setTextAlign(Align.CENTER); 660 final float hintX = keyWidth - mKeyHintLetterPadding 661 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 662 final float hintY = keyHeight - mKeyPopupHintLetterPadding; 663 canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); 664 665 if (LatinImeLogger.sVISUALDEBUG) { 666 final Paint line = new Paint(); 667 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 668 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 669 } 670 } 671 672 private static int getCharGeometryCacheKey(char referenceChar, Paint paint) { 673 final int labelSize = (int)paint.getTextSize(); 674 final Typeface face = paint.getTypeface(); 675 final int codePointOffset = referenceChar << 15; 676 if (face == Typeface.DEFAULT) { 677 return codePointOffset + labelSize; 678 } else if (face == Typeface.DEFAULT_BOLD) { 679 return codePointOffset + labelSize + 0x1000; 680 } else if (face == Typeface.MONOSPACE) { 681 return codePointOffset + labelSize + 0x2000; 682 } else { 683 return codePointOffset + labelSize; 684 } 685 } 686 687 // Working variable for the following methods. 688 private final Rect mTextBounds = new Rect(); 689 690 private float getCharHeight(char[] referenceChar, Paint paint) { 691 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 692 final Float cachedValue = sTextHeightCache.get(key); 693 if (cachedValue != null) 694 return cachedValue; 695 696 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 697 final float height = mTextBounds.height(); 698 sTextHeightCache.put(key, height); 699 return height; 700 } 701 702 private float getCharWidth(char[] referenceChar, Paint paint) { 703 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 704 final Float cachedValue = sTextWidthCache.get(key); 705 if (cachedValue != null) 706 return cachedValue; 707 708 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 709 final float width = mTextBounds.width(); 710 sTextWidthCache.put(key, width); 711 return width; 712 } 713 714 public float getLabelWidth(String label, Paint paint) { 715 paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds); 716 return mTextBounds.width(); 717 } 718 719 protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 720 int height) { 721 canvas.translate(x, y); 722 icon.setBounds(0, 0, width, height); 723 icon.draw(canvas); 724 canvas.translate(-x, -y); 725 } 726 727 private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, 728 Paint paint) { 729 paint.setStyle(Paint.Style.STROKE); 730 paint.setStrokeWidth(1.0f); 731 paint.setColor(color); 732 canvas.drawLine(0, y, w, y, paint); 733 } 734 735 private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) { 736 paint.setStyle(Paint.Style.STROKE); 737 paint.setStrokeWidth(1.0f); 738 paint.setColor(color); 739 canvas.drawLine(x, 0, x, h, paint); 740 } 741 742 private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, 743 Paint paint) { 744 paint.setStyle(Paint.Style.STROKE); 745 paint.setStrokeWidth(1.0f); 746 paint.setColor(color); 747 canvas.translate(x, y); 748 canvas.drawRect(0, 0, w, h, paint); 749 canvas.translate(-x, -y); 750 } 751 752 public Paint newDefaultLabelPaint() { 753 final Paint paint = new Paint(); 754 paint.setAntiAlias(true); 755 paint.setTypeface(mKeyDrawParams.mKeyTypeface); 756 paint.setTextSize(mKeyDrawParams.mKeyLabelSize); 757 return paint; 758 } 759 760 public void cancelAllMessages() { 761 mDrawingHandler.cancelAllMessages(); 762 if (mPreviewPlacerView != null) { 763 mPreviewPlacerView.cancelAllMessages(); 764 } 765 } 766 767 private TextView getKeyPreviewText(final int pointerId) { 768 TextView previewText = mKeyPreviewTexts.get(pointerId); 769 if (previewText != null) { 770 return previewText; 771 } 772 final Context context = getContext(); 773 if (mKeyPreviewLayoutId != 0) { 774 previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); 775 } else { 776 previewText = new TextView(context); 777 } 778 mKeyPreviewTexts.put(pointerId, previewText); 779 return previewText; 780 } 781 782 private void dismissAllKeyPreviews() { 783 final int pointerCount = mKeyPreviewTexts.size(); 784 for (int id = 0; id < pointerCount; id++) { 785 final TextView previewText = mKeyPreviewTexts.get(id); 786 if (previewText != null) { 787 previewText.setVisibility(INVISIBLE); 788 } 789 } 790 PointerTracker.setReleasedKeyGraphicsToAllKeys(); 791 } 792 793 @Override 794 public void dismissKeyPreview(PointerTracker tracker) { 795 mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 796 } 797 798 private void addKeyPreview(TextView keyPreview) { 799 locatePreviewPlacerView(); 800 mPreviewPlacerView.addView( 801 keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); 802 } 803 804 private void locatePreviewPlacerView() { 805 if (mPreviewPlacerView.getParent() != null) { 806 return; 807 } 808 final int[] viewOrigin = new int[2]; 809 getLocationInWindow(viewOrigin); 810 mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]); 811 final View rootView = getRootView(); 812 if (rootView == null) { 813 Log.w(TAG, "Cannot find root view"); 814 return; 815 } 816 final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); 817 // Note: It'd be very weird if we get null by android.R.id.content. 818 if (windowContentView == null) { 819 Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); 820 } else { 821 windowContentView.addView(mPreviewPlacerView); 822 } 823 } 824 825 public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) { 826 locatePreviewPlacerView(); 827 mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText); 828 } 829 830 public void dismissGestureFloatingPreviewText() { 831 locatePreviewPlacerView(); 832 mPreviewPlacerView.dismissGestureFloatingPreviewText(); 833 } 834 835 @Override 836 public void showGesturePreviewTrail(PointerTracker tracker) { 837 locatePreviewPlacerView(); 838 mPreviewPlacerView.invalidatePointer(tracker); 839 } 840 841 @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16 842 @Override 843 public void showKeyPreview(PointerTracker tracker) { 844 final KeyPreviewDrawParams params = mKeyPreviewDrawParams; 845 if (!mShowKeyPreviewPopup) { 846 params.mPreviewVisibleOffset = -mKeyboard.mVerticalGap; 847 return; 848 } 849 850 final TextView previewText = getKeyPreviewText(tracker.mPointerId); 851 // If the key preview has no parent view yet, add it to the ViewGroup which can place 852 // key preview absolutely in SoftInputWindow. 853 if (previewText.getParent() == null) { 854 addKeyPreview(previewText); 855 } 856 857 mDrawingHandler.cancelDismissKeyPreview(tracker); 858 final Key key = tracker.getKey(); 859 // If key is invalid or IME is already closed, we must not show key preview. 860 // Trying to show key preview while root window is closed causes 861 // WindowManager.BadTokenException. 862 if (key == null) 863 return; 864 865 final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel; 866 // What we show as preview should match what we show on a key top in onDraw(). 867 if (label != null) { 868 // TODO Should take care of temporaryShiftLabel here. 869 previewText.setCompoundDrawables(null, null, null, null); 870 if (StringUtils.codePointCount(label) > 1) { 871 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize); 872 previewText.setTypeface(Typeface.DEFAULT_BOLD); 873 } else { 874 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize); 875 previewText.setTypeface(params.mKeyTypeface); 876 } 877 previewText.setText(label); 878 } else { 879 previewText.setCompoundDrawables(null, null, null, 880 key.getPreviewIcon(mKeyboard.mIconsSet)); 881 previewText.setText(null); 882 } 883 previewText.setBackgroundDrawable(params.mPreviewBackground); 884 885 previewText.measure( 886 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 887 final int keyDrawWidth = key.getDrawWidth(); 888 final int previewWidth = previewText.getMeasuredWidth(); 889 final int previewHeight = params.mPreviewHeight; 890 // The width and height of visible part of the key preview background. The content marker 891 // of the background 9-patch have to cover the visible part of the background. 892 params.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() 893 - previewText.getPaddingRight(); 894 params.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() 895 - previewText.getPaddingBottom(); 896 // The distance between the top edge of the parent key and the bottom of the visible part 897 // of the key preview background. 898 params.mPreviewVisibleOffset = params.mPreviewOffset - previewText.getPaddingBottom(); 899 getLocationInWindow(params.mCoordinates); 900 // The key preview is horizontally aligned with the center of the visible part of the 901 // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and 902 // the left/right background is used if such background is specified. 903 int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + params.mCoordinates[0]; 904 if (previewX < 0) { 905 previewX = 0; 906 if (params.mPreviewLeftBackground != null) { 907 previewText.setBackgroundDrawable(params.mPreviewLeftBackground); 908 } 909 } else if (previewX > getWidth() - previewWidth) { 910 previewX = getWidth() - previewWidth; 911 if (params.mPreviewRightBackground != null) { 912 previewText.setBackgroundDrawable(params.mPreviewRightBackground); 913 } 914 } 915 // The key preview is placed vertically above the top edge of the parent key with an 916 // arbitrary offset. 917 final int previewY = key.mY - previewHeight + params.mPreviewOffset 918 + params.mCoordinates[1]; 919 920 // Set the preview background state 921 previewText.getBackground().setState( 922 key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 923 previewText.setTextColor(params.mPreviewTextColor); 924 ViewLayoutUtils.placeViewAt( 925 previewText, previewX, previewY, previewWidth, previewHeight); 926 previewText.setVisibility(VISIBLE); 927 } 928 929 /** 930 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 931 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 932 * draws the cached buffer. 933 * @see #invalidateKey(Key) 934 */ 935 public void invalidateAllKeys() { 936 mInvalidatedKeys.clear(); 937 mInvalidateAllKeys = true; 938 invalidate(); 939 } 940 941 /** 942 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 943 * one key is changing it's content. Any changes that affect the position or size of the key 944 * may not be honored. 945 * @param key key in the attached {@link Keyboard}. 946 * @see #invalidateAllKeys 947 */ 948 @Override 949 public void invalidateKey(Key key) { 950 if (mInvalidateAllKeys) return; 951 if (key == null) return; 952 mInvalidatedKeys.add(key); 953 final int x = key.mX + getPaddingLeft(); 954 final int y = key.mY + getPaddingTop(); 955 invalidate(x, y, x + key.mWidth, y + key.mHeight); 956 } 957 958 public void closing() { 959 dismissAllKeyPreviews(); 960 cancelAllMessages(); 961 962 mInvalidateAllKeys = true; 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 mPreviewPlacerView.removeAllViews(); 981 freeOffscreenBuffer(); 982 } 983} 984