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