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