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