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