KeyboardView.java revision 936371e64fdbbbaa012526bd4d6df9e03682ab8d
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 final Canvas mOffscreenCanvas = new Canvas(); 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 // TODO: Stop using the offscreen canvas even when in software rendering 369 mOffscreenCanvas.setBitmap(mOffscreenBuffer); 370 } 371 onDrawKeyboard(mOffscreenCanvas); 372 } 373 canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); 374 } 375 376 private boolean maybeAllocateOffscreenBuffer() { 377 final int width = getWidth(); 378 final int height = getHeight(); 379 if (width == 0 || height == 0) { 380 return false; 381 } 382 if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width 383 && mOffscreenBuffer.getHeight() == height) { 384 return false; 385 } 386 freeOffscreenBuffer(); 387 mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 388 return true; 389 } 390 391 private void freeOffscreenBuffer() { 392 if (mOffscreenBuffer != null) { 393 mOffscreenBuffer.recycle(); 394 mOffscreenBuffer = null; 395 } 396 } 397 398 private void onDrawKeyboard(final Canvas canvas) { 399 if (mKeyboard == null) return; 400 401 final int width = getWidth(); 402 final int height = getHeight(); 403 final Paint paint = mPaint; 404 405 // Calculate clip region and set. 406 final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty(); 407 final boolean isHardwareAccelerated = canvas.isHardwareAccelerated(); 408 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 409 if (drawAllKeys || isHardwareAccelerated) { 410 mClipRegion.set(0, 0, width, height); 411 } else { 412 mClipRegion.setEmpty(); 413 for (final Key key : mInvalidatedKeys) { 414 if (mKeyboard.hasKey(key)) { 415 final int x = key.mX + getPaddingLeft(); 416 final int y = key.mY + getPaddingTop(); 417 mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight); 418 mClipRegion.union(mWorkingRect); 419 } 420 } 421 } 422 if (!isHardwareAccelerated) { 423 canvas.clipRegion(mClipRegion, Region.Op.REPLACE); 424 // Draw keyboard background. 425 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 426 final Drawable background = getBackground(); 427 if (background != null) { 428 background.draw(canvas); 429 } 430 } 431 432 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 433 if (drawAllKeys || isHardwareAccelerated) { 434 // Draw all keys. 435 for (final Key key : mKeyboard.mKeys) { 436 onDrawKey(key, canvas, paint); 437 } 438 } else { 439 // Draw invalidated keys. 440 for (final Key key : mInvalidatedKeys) { 441 if (mKeyboard.hasKey(key)) { 442 onDrawKey(key, canvas, paint); 443 } 444 } 445 } 446 447 // Overlay a dark rectangle to dim. 448 if (mNeedsToDimEntireKeyboard) { 449 paint.setColor(Color.BLACK); 450 paint.setAlpha(mBackgroundDimAlpha); 451 // Note: clipRegion() above is in effect if it was called. 452 canvas.drawRect(0, 0, width, height, paint); 453 } 454 455 // ResearchLogging indicator. 456 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger, 457 // and remove this call. 458 if (ProductionFlag.IS_EXPERIMENTAL) { 459 ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height); 460 } 461 462 mInvalidatedKeys.clear(); 463 mInvalidateAllKeys = false; 464 } 465 466 public void dimEntireKeyboard(final boolean dimmed) { 467 final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; 468 mNeedsToDimEntireKeyboard = dimmed; 469 if (needsRedrawing) { 470 invalidateAllKeys(); 471 } 472 } 473 474 private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) { 475 final int keyDrawX = key.getDrawX() + getPaddingLeft(); 476 final int keyDrawY = key.mY + getPaddingTop(); 477 canvas.translate(keyDrawX, keyDrawY); 478 479 final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap; 480 final KeyVisualAttributes attr = key.mKeyVisualAttributes; 481 final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr); 482 params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; 483 484 if (!key.isSpacer()) { 485 onDrawKeyBackground(key, canvas); 486 } 487 onDrawKeyTopVisuals(key, canvas, paint, params); 488 489 canvas.translate(-keyDrawX, -keyDrawY); 490 } 491 492 // Draw key background. 493 protected void onDrawKeyBackground(final Key key, final Canvas canvas) { 494 final Rect padding = mKeyBackgroundPadding; 495 final int bgWidth = key.getDrawWidth() + padding.left + padding.right; 496 final int bgHeight = key.mHeight + padding.top + padding.bottom; 497 final int bgX = -padding.left; 498 final int bgY = -padding.top; 499 final int[] drawableState = key.getCurrentDrawableState(); 500 final Drawable background = mKeyBackground; 501 background.setState(drawableState); 502 final Rect bounds = background.getBounds(); 503 if (bgWidth != bounds.right || bgHeight != bounds.bottom) { 504 background.setBounds(0, 0, bgWidth, bgHeight); 505 } 506 canvas.translate(bgX, bgY); 507 background.draw(canvas); 508 if (LatinImeLogger.sVISUALDEBUG) { 509 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); 510 } 511 canvas.translate(-bgX, -bgY); 512 } 513 514 // Draw key top visuals. 515 protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint, 516 final KeyDrawParams params) { 517 final int keyWidth = key.getDrawWidth(); 518 final int keyHeight = key.mHeight; 519 final float centerX = keyWidth * 0.5f; 520 final float centerY = keyHeight * 0.5f; 521 522 if (LatinImeLogger.sVISUALDEBUG) { 523 drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); 524 } 525 526 // Draw key label. 527 final Drawable icon = key.getIcon(mKeyboard.mIconsSet, params.mAnimAlpha); 528 float positionX = centerX; 529 if (key.mLabel != null) { 530 final String label = key.mLabel; 531 paint.setTypeface(key.selectTypeface(params)); 532 paint.setTextSize(key.selectTextSize(params)); 533 final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); 534 final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); 535 536 // Vertical label text alignment. 537 final float baseline = centerY + labelCharHeight / 2; 538 539 // Horizontal label text alignment 540 float labelWidth = 0; 541 if (key.isAlignLeft()) { 542 positionX = mKeyLabelHorizontalPadding; 543 paint.setTextAlign(Align.LEFT); 544 } else if (key.isAlignRight()) { 545 positionX = keyWidth - mKeyLabelHorizontalPadding; 546 paint.setTextAlign(Align.RIGHT); 547 } else if (key.isAlignLeftOfCenter()) { 548 // TODO: Parameterise this? 549 positionX = centerX - labelCharWidth * 7 / 4; 550 paint.setTextAlign(Align.LEFT); 551 } else if (key.hasLabelWithIconLeft() && icon != null) { 552 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 553 + LABEL_ICON_MARGIN * keyWidth; 554 positionX = centerX + labelWidth / 2; 555 paint.setTextAlign(Align.RIGHT); 556 } else if (key.hasLabelWithIconRight() && icon != null) { 557 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 558 + LABEL_ICON_MARGIN * keyWidth; 559 positionX = centerX - labelWidth / 2; 560 paint.setTextAlign(Align.LEFT); 561 } else { 562 positionX = centerX; 563 paint.setTextAlign(Align.CENTER); 564 } 565 if (key.needsXScale()) { 566 paint.setTextScaleX( 567 Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); 568 } 569 570 paint.setColor(key.selectTextColor(params)); 571 if (key.isEnabled()) { 572 // Set a drop shadow for the text 573 paint.setShadowLayer(mKeyTextShadowRadius, 0, 0, params.mTextShadowColor); 574 } else { 575 // Make label invisible 576 paint.setColor(Color.TRANSPARENT); 577 } 578 blendAlpha(paint, params.mAnimAlpha); 579 canvas.drawText(label, 0, label.length(), positionX, baseline, paint); 580 // Turn off drop shadow and reset x-scale. 581 paint.setShadowLayer(0, 0, 0, 0); 582 paint.setTextScaleX(1.0f); 583 584 if (icon != null) { 585 final int iconWidth = icon.getIntrinsicWidth(); 586 final int iconHeight = icon.getIntrinsicHeight(); 587 final int iconY = (keyHeight - iconHeight) / 2; 588 if (key.hasLabelWithIconLeft()) { 589 final int iconX = (int)(centerX - labelWidth / 2); 590 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 591 } else if (key.hasLabelWithIconRight()) { 592 final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); 593 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 594 } 595 } 596 597 if (LatinImeLogger.sVISUALDEBUG) { 598 final Paint line = new Paint(); 599 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line); 600 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line); 601 } 602 } 603 604 // Draw hint label. 605 if (key.mHintLabel != null) { 606 final String hintLabel = key.mHintLabel; 607 paint.setTextSize(key.selectHintTextSize(params)); 608 paint.setColor(key.selectHintTextColor(params)); 609 blendAlpha(paint, params.mAnimAlpha); 610 final float hintX, hintY; 611 if (key.hasHintLabel()) { 612 // The hint label is placed just right of the key label. Used mainly on 613 // "phone number" layout. 614 // TODO: Generalize the following calculations. 615 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; 616 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 617 paint.setTextAlign(Align.LEFT); 618 } else if (key.hasShiftedLetterHint()) { 619 // The hint label is placed at top-right corner of the key. Used mainly on tablet. 620 hintX = keyWidth - mKeyShiftedLetterHintPadding 621 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 622 paint.getFontMetrics(mFontMetrics); 623 hintY = -mFontMetrics.top; 624 paint.setTextAlign(Align.CENTER); 625 } else { // key.hasHintLetter() 626 // The hint letter is placed at top-right corner of the key. Used mainly on phone. 627 hintX = keyWidth - mKeyHintLetterPadding 628 - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; 629 hintY = -paint.ascent(); 630 paint.setTextAlign(Align.CENTER); 631 } 632 canvas.drawText(hintLabel, 0, hintLabel.length(), hintX, hintY, paint); 633 634 if (LatinImeLogger.sVISUALDEBUG) { 635 final Paint line = new Paint(); 636 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 637 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 638 } 639 } 640 641 // Draw key icon. 642 if (key.mLabel == null && icon != null) { 643 final int iconWidth = Math.min(icon.getIntrinsicWidth(), keyWidth); 644 final int iconHeight = icon.getIntrinsicHeight(); 645 final int iconX, alignX; 646 final int iconY = (keyHeight - iconHeight) / 2; 647 if (key.isAlignLeft()) { 648 iconX = mKeyLabelHorizontalPadding; 649 alignX = iconX; 650 } else if (key.isAlignRight()) { 651 iconX = keyWidth - mKeyLabelHorizontalPadding - iconWidth; 652 alignX = iconX + iconWidth; 653 } else { // Align center 654 iconX = (keyWidth - iconWidth) / 2; 655 alignX = iconX + iconWidth / 2; 656 } 657 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 658 659 if (LatinImeLogger.sVISUALDEBUG) { 660 final Paint line = new Paint(); 661 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line); 662 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); 663 } 664 } 665 666 if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) { 667 drawKeyPopupHint(key, canvas, paint, params); 668 } 669 } 670 671 // Draw popup hint "..." at the bottom right corner of the key. 672 protected void drawKeyPopupHint(final Key key, final Canvas canvas, final Paint paint, 673 final KeyDrawParams params) { 674 final int keyWidth = key.getDrawWidth(); 675 final int keyHeight = key.mHeight; 676 677 paint.setTypeface(params.mTypeface); 678 paint.setTextSize(params.mHintLetterSize); 679 paint.setColor(params.mHintLabelColor); 680 paint.setTextAlign(Align.CENTER); 681 final float hintX = keyWidth - mKeyHintLetterPadding 682 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 683 final float hintY = keyHeight - mKeyPopupHintLetterPadding; 684 canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); 685 686 if (LatinImeLogger.sVISUALDEBUG) { 687 final Paint line = new Paint(); 688 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 689 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 690 } 691 } 692 693 private static int getCharGeometryCacheKey(final char referenceChar, final Paint paint) { 694 final int labelSize = (int)paint.getTextSize(); 695 final Typeface face = paint.getTypeface(); 696 final int codePointOffset = referenceChar << 15; 697 if (face == Typeface.DEFAULT) { 698 return codePointOffset + labelSize; 699 } else if (face == Typeface.DEFAULT_BOLD) { 700 return codePointOffset + labelSize + 0x1000; 701 } else if (face == Typeface.MONOSPACE) { 702 return codePointOffset + labelSize + 0x2000; 703 } else { 704 return codePointOffset + labelSize; 705 } 706 } 707 708 // Working variable for the following methods. 709 private final Rect mTextBounds = new Rect(); 710 711 private float getCharHeight(final char[] referenceChar, final Paint paint) { 712 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 713 final Float cachedValue = sTextHeightCache.get(key); 714 if (cachedValue != null) 715 return cachedValue; 716 717 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 718 final float height = mTextBounds.height(); 719 sTextHeightCache.put(key, height); 720 return height; 721 } 722 723 private float getCharWidth(final char[] referenceChar, final Paint paint) { 724 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 725 final Float cachedValue = sTextWidthCache.get(key); 726 if (cachedValue != null) 727 return cachedValue; 728 729 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 730 final float width = mTextBounds.width(); 731 sTextWidthCache.put(key, width); 732 return width; 733 } 734 735 public float getLabelWidth(final String label, final Paint paint) { 736 paint.getTextBounds(label, 0, label.length(), mTextBounds); 737 return mTextBounds.width(); 738 } 739 740 protected static void drawIcon(final Canvas canvas, final Drawable icon, final int x, 741 final int y, final int width, final int height) { 742 canvas.translate(x, y); 743 icon.setBounds(0, 0, width, height); 744 icon.draw(canvas); 745 canvas.translate(-x, -y); 746 } 747 748 private static void drawHorizontalLine(final Canvas canvas, final float y, final float w, 749 final int color, final Paint paint) { 750 paint.setStyle(Paint.Style.STROKE); 751 paint.setStrokeWidth(1.0f); 752 paint.setColor(color); 753 canvas.drawLine(0, y, w, y, paint); 754 } 755 756 private static void drawVerticalLine(final Canvas canvas, final float x, final float h, 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(x, 0, x, h, paint); 762 } 763 764 private static void drawRectangle(final Canvas canvas, final float x, final float y, 765 final float w, final float h, final int color, final Paint paint) { 766 paint.setStyle(Paint.Style.STROKE); 767 paint.setStrokeWidth(1.0f); 768 paint.setColor(color); 769 canvas.translate(x, y); 770 canvas.drawRect(0, 0, w, h, paint); 771 canvas.translate(-x, -y); 772 } 773 774 public Paint newDefaultLabelPaint() { 775 final Paint paint = new Paint(); 776 paint.setAntiAlias(true); 777 paint.setTypeface(mKeyDrawParams.mTypeface); 778 paint.setTextSize(mKeyDrawParams.mLabelSize); 779 return paint; 780 } 781 782 public void cancelAllMessages() { 783 mDrawingHandler.cancelAllMessages(); 784 } 785 786 private TextView getKeyPreviewText(final int pointerId) { 787 TextView previewText = mKeyPreviewTexts.get(pointerId); 788 if (previewText != null) { 789 return previewText; 790 } 791 final Context context = getContext(); 792 if (mKeyPreviewLayoutId != 0) { 793 previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); 794 } else { 795 previewText = new TextView(context); 796 } 797 mKeyPreviewTexts.put(pointerId, previewText); 798 return previewText; 799 } 800 801 private void dismissAllKeyPreviews() { 802 final int pointerCount = mKeyPreviewTexts.size(); 803 for (int id = 0; id < pointerCount; id++) { 804 final TextView previewText = mKeyPreviewTexts.get(id); 805 if (previewText != null) { 806 previewText.setVisibility(INVISIBLE); 807 } 808 } 809 PointerTracker.setReleasedKeyGraphicsToAllKeys(); 810 } 811 812 @Override 813 public void dismissKeyPreview(final PointerTracker tracker) { 814 mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 815 } 816 817 private void addKeyPreview(final TextView keyPreview) { 818 locatePreviewPlacerView(); 819 mPreviewPlacerView.addView( 820 keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0)); 821 } 822 823 private void locatePreviewPlacerView() { 824 if (mPreviewPlacerView.getParent() != null) { 825 return; 826 } 827 final int[] viewOrigin = new int[2]; 828 getLocationInWindow(viewOrigin); 829 mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]); 830 final View rootView = getRootView(); 831 if (rootView == null) { 832 Log.w(TAG, "Cannot find root view"); 833 return; 834 } 835 final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); 836 // Note: It'd be very weird if we get null by android.R.id.content. 837 if (windowContentView == null) { 838 Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); 839 } else { 840 windowContentView.addView(mPreviewPlacerView); 841 } 842 } 843 844 public void showGestureFloatingPreviewText(final String gestureFloatingPreviewText) { 845 locatePreviewPlacerView(); 846 mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText); 847 } 848 849 public void dismissGestureFloatingPreviewText() { 850 locatePreviewPlacerView(); 851 mPreviewPlacerView.dismissGestureFloatingPreviewText(); 852 } 853 854 @Override 855 public void showGesturePreviewTrail(final PointerTracker tracker, 856 final boolean isOldestTracker) { 857 locatePreviewPlacerView(); 858 mPreviewPlacerView.invalidatePointer(tracker, isOldestTracker); 859 } 860 861 @Override 862 public void showKeyPreview(final PointerTracker tracker) { 863 final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; 864 if (!mShowKeyPreviewPopup) { 865 previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap; 866 return; 867 } 868 869 final TextView previewText = getKeyPreviewText(tracker.mPointerId); 870 // If the key preview has no parent view yet, add it to the ViewGroup which can place 871 // key preview absolutely in SoftInputWindow. 872 if (previewText.getParent() == null) { 873 addKeyPreview(previewText); 874 } 875 876 mDrawingHandler.cancelDismissKeyPreview(tracker); 877 final Key key = tracker.getKey(); 878 // If key is invalid or IME is already closed, we must not show key preview. 879 // Trying to show key preview while root window is closed causes 880 // WindowManager.BadTokenException. 881 if (key == null) { 882 return; 883 } 884 885 final KeyDrawParams drawParams = mKeyDrawParams; 886 previewText.setTextColor(drawParams.mPreviewTextColor); 887 final Drawable background = previewText.getBackground(); 888 if (background != null) { 889 background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE); 890 background.setAlpha(PREVIEW_ALPHA); 891 } 892 final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel; 893 // What we show as preview should match what we show on a key top in onDraw(). 894 if (label != null) { 895 // TODO Should take care of temporaryShiftLabel here. 896 previewText.setCompoundDrawables(null, null, null, null); 897 if (StringUtils.codePointCount(label) > 1) { 898 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize); 899 previewText.setTypeface(Typeface.DEFAULT_BOLD); 900 } else { 901 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize); 902 previewText.setTypeface(key.selectTypeface(drawParams)); 903 } 904 previewText.setText(label); 905 } else { 906 previewText.setCompoundDrawables(null, null, null, 907 key.getPreviewIcon(mKeyboard.mIconsSet)); 908 previewText.setText(null); 909 } 910 911 previewText.measure( 912 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 913 final int keyDrawWidth = key.getDrawWidth(); 914 final int previewWidth = previewText.getMeasuredWidth(); 915 final int previewHeight = mPreviewHeight; 916 // The width and height of visible part of the key preview background. The content marker 917 // of the background 9-patch have to cover the visible part of the background. 918 previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() 919 - previewText.getPaddingRight(); 920 previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() 921 - previewText.getPaddingBottom(); 922 // The distance between the top edge of the parent key and the bottom of the visible part 923 // of the key preview background. 924 previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom(); 925 getLocationInWindow(mCoordinates); 926 // The key preview is horizontally aligned with the center of the visible part of the 927 // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and 928 // the left/right background is used if such background is specified. 929 final int statePosition; 930 int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0]; 931 if (previewX < 0) { 932 previewX = 0; 933 statePosition = STATE_LEFT; 934 } else if (previewX > getWidth() - previewWidth) { 935 previewX = getWidth() - previewWidth; 936 statePosition = STATE_RIGHT; 937 } else { 938 statePosition = STATE_MIDDLE; 939 } 940 // The key preview is placed vertically above the top edge of the parent key with an 941 // arbitrary offset. 942 final int previewY = key.mY - previewHeight + mPreviewOffset + mCoordinates[1]; 943 944 if (background != null) { 945 final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL; 946 background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]); 947 } 948 ViewLayoutUtils.placeViewAt( 949 previewText, previewX, previewY, previewWidth, previewHeight); 950 previewText.setVisibility(VISIBLE); 951 } 952 953 /** 954 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 955 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 956 * draws the cached buffer. 957 * @see #invalidateKey(Key) 958 */ 959 public void invalidateAllKeys() { 960 mInvalidatedKeys.clear(); 961 mInvalidateAllKeys = true; 962 invalidate(); 963 } 964 965 /** 966 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 967 * one key is changing it's content. Any changes that affect the position or size of the key 968 * may not be honored. 969 * @param key key in the attached {@link Keyboard}. 970 * @see #invalidateAllKeys 971 */ 972 @Override 973 public void invalidateKey(final Key key) { 974 if (mInvalidateAllKeys) return; 975 if (key == null) return; 976 mInvalidatedKeys.add(key); 977 final int x = key.mX + getPaddingLeft(); 978 final int y = key.mY + getPaddingTop(); 979 invalidate(x, y, x + key.mWidth, y + key.mHeight); 980 } 981 982 public void closing() { 983 dismissAllKeyPreviews(); 984 cancelAllMessages(); 985 986 mInvalidateAllKeys = true; 987 requestLayout(); 988 } 989 990 @Override 991 public boolean dismissMoreKeysPanel() { 992 return false; 993 } 994 995 public void purgeKeyboardAndClosing() { 996 mKeyboard = null; 997 closing(); 998 } 999 1000 @Override 1001 protected void onDetachedFromWindow() { 1002 super.onDetachedFromWindow(); 1003 closing(); 1004 mPreviewPlacerView.removeAllViews(); 1005 freeOffscreenBuffer(); 1006 } 1007} 1008