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