KeyboardView.java revision dc34da218a22489d92d1015e9e5dac8d951b89f4
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_keyPreviewBackground 63 * @attr ref R.styleable#KeyboardView_keyPreviewLeftBackground 64 * @attr ref R.styleable#KeyboardView_keyPreviewRightBackground 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_gestureFloatingPreviewTextShadingColor 78 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadingBorder 79 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowColor 80 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextShadowBorder 81 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorColor 82 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextConnectorWidth 83 * @attr ref R.styleable#KeyboardView_gestureFloatingPreviewTextLingerTimeout 84 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutStartDelay 85 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailFadeoutDuration 86 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailUpdateInterval 87 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailColor 88 * @attr ref R.styleable#KeyboardView_gesturePreviewTrailWidth 89 * @attr ref R.styleable#KeyboardView_verticalCorrection 90 * @attr ref R.styleable#Keyboard_Key_keyTypeface 91 * @attr ref R.styleable#Keyboard_Key_keyLetterSize 92 * @attr ref R.styleable#Keyboard_Key_keyLabelSize 93 * @attr ref R.styleable#Keyboard_Key_keyLargeLetterRatio 94 * @attr ref R.styleable#Keyboard_Key_keyLargeLabelRatio 95 * @attr ref R.styleable#Keyboard_Key_keyHintLetterRatio 96 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintRatio 97 * @attr ref R.styleable#Keyboard_Key_keyHintLabelRatio 98 * @attr ref R.styleable#Keyboard_Key_keyPreviewTextRatio 99 * @attr ref R.styleable#Keyboard_Key_keyTextColor 100 * @attr ref R.styleable#Keyboard_Key_keyTextColorDisabled 101 * @attr ref R.styleable#Keyboard_Key_keyTextShadowColor 102 * @attr ref R.styleable#Keyboard_Key_keyHintLetterColor 103 * @attr ref R.styleable#Keyboard_Key_keyHintLabelColor 104 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintInactivatedColor 105 * @attr ref R.styleable#Keyboard_Key_keyShiftedLetterHintActivatedColor 106 * @attr ref R.styleable#Keyboard_Key_keyPreviewTextColor 107 */ 108public class KeyboardView extends View implements PointerTracker.DrawingProxy { 109 private static final String TAG = KeyboardView.class.getSimpleName(); 110 111 // Miscellaneous constants 112 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 113 114 // XML attributes 115 private final KeyVisualAttributes mKeyVisualAttributes; 116 private final int mKeyLabelHorizontalPadding; 117 private final float mKeyHintLetterPadding; 118 private final float mKeyPopupHintLetterPadding; 119 private final float mKeyShiftedLetterHintPadding; 120 private final float mKeyTextShadowRadius; 121 protected final float mVerticalCorrection; 122 protected final int mMoreKeysLayout; 123 protected final Drawable mKeyBackground; 124 protected final Rect mKeyBackgroundPadding = new Rect(); 125 private final int mBackgroundDimAlpha; 126 127 // HORIZONTAL ELLIPSIS "...", character for popup hint. 128 private static final String POPUP_HINT_CHAR = "\u2026"; 129 130 // Margin between the label and the icon on a key that has both of them. 131 // Specified by the fraction of the key width. 132 // TODO: Use resource parameter for this value. 133 private static final float LABEL_ICON_MARGIN = 0.05f; 134 135 // The maximum key label width in the proportion to the key width. 136 private static final float MAX_LABEL_RATIO = 0.90f; 137 138 // Main keyboard 139 private Keyboard mKeyboard; 140 protected final KeyDrawParams mKeyDrawParams = new KeyDrawParams(); 141 142 // Key preview 143 private static final int PREVIEW_ALPHA = 240; 144 private final int mKeyPreviewLayoutId; 145 private final Drawable mPreviewBackground; 146 private final Drawable mPreviewLeftBackground; 147 private final Drawable mPreviewRightBackground; 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 private final PreviewPlacerView mPreviewPlacerView; 156 157 // Drawing 158 /** True if the entire keyboard needs to be dimmed. */ 159 private boolean mNeedsToDimEntireKeyboard; 160 /** True if all keys should be drawn */ 161 private boolean mInvalidateAllKeys; 162 /** The keys that should be drawn */ 163 private final HashSet<Key> mInvalidatedKeys = CollectionUtils.newHashSet(); 164 /** The working rectangle variable */ 165 private final Rect mWorkingRect = new Rect(); 166 /** The keyboard bitmap buffer for faster updates */ 167 /** The clip region to draw keys */ 168 private final Region mClipRegion = new Region(); 169 private Bitmap mOffscreenBuffer; 170 /** The canvas for the above mutable keyboard bitmap */ 171 private Canvas mOffscreenCanvas; 172 private final Paint mPaint = new Paint(); 173 private final Paint.FontMetrics mFontMetrics = new Paint.FontMetrics(); 174 // This sparse array caches key label text height in pixel indexed by key label text size. 175 private static final SparseArray<Float> sTextHeightCache = CollectionUtils.newSparseArray(); 176 // This sparse array caches key label text width in pixel indexed by key label text size. 177 private static final SparseArray<Float> sTextWidthCache = CollectionUtils.newSparseArray(); 178 private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; 179 private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; 180 181 private final DrawingHandler mDrawingHandler = new DrawingHandler(this); 182 183 public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> { 184 private static final int MSG_DISMISS_KEY_PREVIEW = 0; 185 186 public DrawingHandler(final KeyboardView outerInstance) { 187 super(outerInstance); 188 } 189 190 @Override 191 public void handleMessage(final Message msg) { 192 final KeyboardView keyboardView = getOuterInstance(); 193 if (keyboardView == null) return; 194 final PointerTracker tracker = (PointerTracker) msg.obj; 195 switch (msg.what) { 196 case MSG_DISMISS_KEY_PREVIEW: 197 final TextView previewText = keyboardView.mKeyPreviewTexts.get(tracker.mPointerId); 198 if (previewText != null) { 199 previewText.setVisibility(INVISIBLE); 200 } 201 break; 202 } 203 } 204 205 public void dismissKeyPreview(final long delay, final PointerTracker tracker) { 206 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 207 } 208 209 public void cancelDismissKeyPreview(final PointerTracker tracker) { 210 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 211 } 212 213 private void cancelAllDismissKeyPreviews() { 214 removeMessages(MSG_DISMISS_KEY_PREVIEW); 215 } 216 217 public void cancelAllMessages() { 218 cancelAllDismissKeyPreviews(); 219 } 220 } 221 222 public KeyboardView(final Context context, final AttributeSet attrs) { 223 this(context, attrs, R.attr.keyboardViewStyle); 224 } 225 226 public KeyboardView(final Context context, final AttributeSet attrs, final int defStyle) { 227 super(context, attrs, defStyle); 228 229 final TypedArray keyboardViewAttr = context.obtainStyledAttributes(attrs, 230 R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 231 mKeyBackground = keyboardViewAttr.getDrawable(R.styleable.KeyboardView_keyBackground); 232 mKeyBackground.getPadding(mKeyBackgroundPadding); 233 mPreviewBackground = keyboardViewAttr.getDrawable( 234 R.styleable.KeyboardView_keyPreviewBackground); 235 mPreviewLeftBackground = keyboardViewAttr.getDrawable( 236 R.styleable.KeyboardView_keyPreviewLeftBackground); 237 mPreviewRightBackground = keyboardViewAttr.getDrawable( 238 R.styleable.KeyboardView_keyPreviewRightBackground); 239 setAlpha(mPreviewBackground, PREVIEW_ALPHA); 240 setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); 241 setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); 242 mPreviewOffset = keyboardViewAttr.getDimensionPixelOffset( 243 R.styleable.KeyboardView_keyPreviewOffset, 0); 244 mPreviewHeight = keyboardViewAttr.getDimensionPixelSize( 245 R.styleable.KeyboardView_keyPreviewHeight, 80); 246 mPreviewLingerTimeout = keyboardViewAttr.getInt( 247 R.styleable.KeyboardView_keyPreviewLingerTimeout, 0); 248 mDelayAfterPreview = mPreviewLingerTimeout; 249 mKeyLabelHorizontalPadding = keyboardViewAttr.getDimensionPixelOffset( 250 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); 251 mKeyHintLetterPadding = keyboardViewAttr.getDimension( 252 R.styleable.KeyboardView_keyHintLetterPadding, 0); 253 mKeyPopupHintLetterPadding = keyboardViewAttr.getDimension( 254 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); 255 mKeyShiftedLetterHintPadding = keyboardViewAttr.getDimension( 256 R.styleable.KeyboardView_keyShiftedLetterHintPadding, 0); 257 mKeyTextShadowRadius = keyboardViewAttr.getFloat( 258 R.styleable.KeyboardView_keyTextShadowRadius, 0.0f); 259 mKeyPreviewLayoutId = keyboardViewAttr.getResourceId( 260 R.styleable.KeyboardView_keyPreviewLayout, 0); 261 if (mKeyPreviewLayoutId == 0) { 262 mShowKeyPreviewPopup = false; 263 } 264 mVerticalCorrection = keyboardViewAttr.getDimension( 265 R.styleable.KeyboardView_verticalCorrection, 0); 266 mMoreKeysLayout = keyboardViewAttr.getResourceId( 267 R.styleable.KeyboardView_moreKeysLayout, 0); 268 mBackgroundDimAlpha = keyboardViewAttr.getInt( 269 R.styleable.KeyboardView_backgroundDimAlpha, 0); 270 keyboardViewAttr.recycle(); 271 272 final TypedArray keyAttr = context.obtainStyledAttributes(attrs, 273 R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView); 274 mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); 275 keyAttr.recycle(); 276 277 mPreviewPlacerView = new PreviewPlacerView(context, attrs); 278 mPaint.setAntiAlias(true); 279 } 280 281 private static void setAlpha(final Drawable drawable, final int alpha) { 282 if (drawable == null) return; 283 drawable.setAlpha(alpha); 284 } 285 286 private static void blendAlpha(final Paint paint, final int alpha) { 287 final int color = paint.getColor(); 288 paint.setARGB((paint.getAlpha() * alpha) / Constants.Color.ALPHA_OPAQUE, 289 Color.red(color), Color.green(color), Color.blue(color)); 290 } 291 292 /** 293 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 294 * view will re-layout itself to accommodate the keyboard. 295 * @see Keyboard 296 * @see #getKeyboard() 297 * @param keyboard the keyboard to display in this view 298 */ 299 public void setKeyboard(final Keyboard keyboard) { 300 mKeyboard = keyboard; 301 LatinImeLogger.onSetKeyboard(keyboard); 302 requestLayout(); 303 invalidateAllKeys(); 304 final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; 305 mKeyDrawParams.updateParams(keyHeight, mKeyVisualAttributes); 306 mKeyDrawParams.updateParams(keyHeight, keyboard.mKeyVisualAttributes); 307 } 308 309 /** 310 * Returns the current keyboard being displayed by this view. 311 * @return the currently attached keyboard 312 * @see #setKeyboard(Keyboard) 313 */ 314 public Keyboard getKeyboard() { 315 return mKeyboard; 316 } 317 318 /** 319 * Enables or disables the key feedback popup. This is a popup that shows a magnified 320 * version of the depressed key. By default the preview is enabled. 321 * @param previewEnabled whether or not to enable the key feedback preview 322 * @param delay the delay after which the preview is dismissed 323 * @see #isKeyPreviewPopupEnabled() 324 */ 325 public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) { 326 mShowKeyPreviewPopup = previewEnabled; 327 mDelayAfterPreview = delay; 328 } 329 330 /** 331 * Returns the enabled state of the key feedback preview 332 * @return whether or not the key feedback preview is enabled 333 * @see #setKeyPreviewPopupEnabled(boolean, int) 334 */ 335 public boolean isKeyPreviewPopupEnabled() { 336 return mShowKeyPreviewPopup; 337 } 338 339 public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail, 340 final boolean drawsGestureFloatingPreviewText) { 341 mPreviewPlacerView.setGesturePreviewMode( 342 drawsGesturePreviewTrail, drawsGestureFloatingPreviewText); 343 } 344 345 @Override 346 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { 347 if (mKeyboard != null) { 348 // The main keyboard expands to the display width. 349 final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom(); 350 setMeasuredDimension(widthMeasureSpec, height); 351 } else { 352 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 353 } 354 } 355 356 @Override 357 public void onDraw(final Canvas canvas) { 358 super.onDraw(canvas); 359 if (canvas.isHardwareAccelerated()) { 360 onDrawKeyboard(canvas); 361 return; 362 } 363 364 final boolean bufferNeedsUpdates = mInvalidateAllKeys || !mInvalidatedKeys.isEmpty(); 365 if (bufferNeedsUpdates || mOffscreenBuffer == null) { 366 if (maybeAllocateOffscreenBuffer()) { 367 mInvalidateAllKeys = true; 368 maybeCreateOffscreenCanvas(); 369 } 370 onDrawKeyboard(mOffscreenCanvas); 371 } 372 canvas.drawBitmap(mOffscreenBuffer, 0, 0, null); 373 } 374 375 private boolean maybeAllocateOffscreenBuffer() { 376 final int width = getWidth(); 377 final int height = getHeight(); 378 if (width == 0 || height == 0) { 379 return false; 380 } 381 if (mOffscreenBuffer != null && mOffscreenBuffer.getWidth() == width 382 && mOffscreenBuffer.getHeight() == height) { 383 return false; 384 } 385 freeOffscreenBuffer(); 386 mOffscreenBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 387 return true; 388 } 389 390 private void freeOffscreenBuffer() { 391 if (mOffscreenBuffer != null) { 392 mOffscreenBuffer.recycle(); 393 mOffscreenBuffer = null; 394 } 395 } 396 397 private void maybeCreateOffscreenCanvas() { 398 // TODO: Stop using the offscreen canvas even when in software rendering 399 if (mOffscreenCanvas != null) { 400 mOffscreenCanvas.setBitmap(mOffscreenBuffer); 401 } else { 402 mOffscreenCanvas = new Canvas(mOffscreenBuffer); 403 } 404 } 405 406 private void onDrawKeyboard(final Canvas canvas) { 407 if (mKeyboard == null) return; 408 409 final int width = getWidth(); 410 final int height = getHeight(); 411 final Paint paint = mPaint; 412 413 // Calculate clip region and set. 414 final boolean drawAllKeys = mInvalidateAllKeys || mInvalidatedKeys.isEmpty(); 415 final boolean isHardwareAccelerated = canvas.isHardwareAccelerated(); 416 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 417 if (drawAllKeys || isHardwareAccelerated) { 418 mClipRegion.set(0, 0, width, height); 419 } else { 420 mClipRegion.setEmpty(); 421 for (final Key key : mInvalidatedKeys) { 422 if (mKeyboard.hasKey(key)) { 423 final int x = key.mX + getPaddingLeft(); 424 final int y = key.mY + getPaddingTop(); 425 mWorkingRect.set(x, y, x + key.mWidth, y + key.mHeight); 426 mClipRegion.union(mWorkingRect); 427 } 428 } 429 } 430 if (!isHardwareAccelerated) { 431 canvas.clipRegion(mClipRegion, Region.Op.REPLACE); 432 // Draw keyboard background. 433 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 434 final Drawable background = getBackground(); 435 if (background != null) { 436 background.draw(canvas); 437 } 438 } 439 440 // TODO: Confirm if it's really required to draw all keys when hardware acceleration is on. 441 if (drawAllKeys || isHardwareAccelerated) { 442 // Draw all keys. 443 for (final Key key : mKeyboard.mKeys) { 444 onDrawKey(key, canvas, paint); 445 } 446 } else { 447 // Draw invalidated keys. 448 for (final Key key : mInvalidatedKeys) { 449 if (mKeyboard.hasKey(key)) { 450 onDrawKey(key, canvas, paint); 451 } 452 } 453 } 454 455 // Overlay a dark rectangle to dim. 456 if (mNeedsToDimEntireKeyboard) { 457 paint.setColor(Color.BLACK); 458 paint.setAlpha(mBackgroundDimAlpha); 459 // Note: clipRegion() above is in effect if it was called. 460 canvas.drawRect(0, 0, width, height, paint); 461 } 462 463 // ResearchLogging indicator. 464 // TODO: Reimplement using a keyboard background image specific to the ResearchLogger, 465 // and remove this call. 466 if (ProductionFlag.IS_EXPERIMENTAL) { 467 ResearchLogger.getInstance().paintIndicator(this, paint, canvas, width, height); 468 } 469 470 mInvalidatedKeys.clear(); 471 mInvalidateAllKeys = false; 472 } 473 474 public void dimEntireKeyboard(final boolean dimmed) { 475 final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed; 476 mNeedsToDimEntireKeyboard = dimmed; 477 if (needsRedrawing) { 478 invalidateAllKeys(); 479 } 480 } 481 482 private void onDrawKey(final Key key, final Canvas canvas, final Paint paint) { 483 final int keyDrawX = key.getDrawX() + getPaddingLeft(); 484 final int keyDrawY = key.mY + getPaddingTop(); 485 canvas.translate(keyDrawX, keyDrawY); 486 487 final int keyHeight = mKeyboard.mMostCommonKeyHeight - mKeyboard.mVerticalGap; 488 final KeyVisualAttributes attr = key.mKeyVisualAttributes; 489 final KeyDrawParams params = mKeyDrawParams.mayCloneAndUpdateParams(keyHeight, attr); 490 params.mAnimAlpha = Constants.Color.ALPHA_OPAQUE; 491 492 if (!key.isSpacer()) { 493 onDrawKeyBackground(key, canvas); 494 } 495 onDrawKeyTopVisuals(key, canvas, paint, params); 496 497 canvas.translate(-keyDrawX, -keyDrawY); 498 } 499 500 // Draw key background. 501 protected void onDrawKeyBackground(Key key, Canvas canvas) { 502 final Rect padding = mKeyBackgroundPadding; 503 final int bgWidth = key.getDrawWidth() + padding.left + padding.right; 504 final int bgHeight = key.mHeight + padding.top + padding.bottom; 505 final int bgX = -padding.left; 506 final int bgY = -padding.top; 507 final int[] drawableState = key.getCurrentDrawableState(); 508 final Drawable background = mKeyBackground; 509 background.setState(drawableState); 510 final Rect bounds = background.getBounds(); 511 if (bgWidth != bounds.right || bgHeight != bounds.bottom) { 512 background.setBounds(0, 0, bgWidth, bgHeight); 513 } 514 canvas.translate(bgX, bgY); 515 background.draw(canvas); 516 if (LatinImeLogger.sVISUALDEBUG) { 517 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); 518 } 519 canvas.translate(-bgX, -bgY); 520 } 521 522 // Draw key top visuals. 523 protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, 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 = icon.getIntrinsicWidth(); 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(Key key, Canvas canvas, Paint paint, KeyDrawParams params) { 680 final int keyWidth = key.getDrawWidth(); 681 final int keyHeight = key.mHeight; 682 683 paint.setTypeface(params.mTypeface); 684 paint.setTextSize(params.mHintLetterSize); 685 paint.setColor(params.mHintLabelColor); 686 paint.setTextAlign(Align.CENTER); 687 final float hintX = keyWidth - mKeyHintLetterPadding 688 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 689 final float hintY = keyHeight - mKeyPopupHintLetterPadding; 690 canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); 691 692 if (LatinImeLogger.sVISUALDEBUG) { 693 final Paint line = new Paint(); 694 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 695 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 696 } 697 } 698 699 private static int getCharGeometryCacheKey(char referenceChar, Paint paint) { 700 final int labelSize = (int)paint.getTextSize(); 701 final Typeface face = paint.getTypeface(); 702 final int codePointOffset = referenceChar << 15; 703 if (face == Typeface.DEFAULT) { 704 return codePointOffset + labelSize; 705 } else if (face == Typeface.DEFAULT_BOLD) { 706 return codePointOffset + labelSize + 0x1000; 707 } else if (face == Typeface.MONOSPACE) { 708 return codePointOffset + labelSize + 0x2000; 709 } else { 710 return codePointOffset + labelSize; 711 } 712 } 713 714 // Working variable for the following methods. 715 private final Rect mTextBounds = new Rect(); 716 717 private float getCharHeight(char[] referenceChar, Paint paint) { 718 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 719 final Float cachedValue = sTextHeightCache.get(key); 720 if (cachedValue != null) 721 return cachedValue; 722 723 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 724 final float height = mTextBounds.height(); 725 sTextHeightCache.put(key, height); 726 return height; 727 } 728 729 private float getCharWidth(char[] referenceChar, Paint paint) { 730 final int key = getCharGeometryCacheKey(referenceChar[0], paint); 731 final Float cachedValue = sTextWidthCache.get(key); 732 if (cachedValue != null) 733 return cachedValue; 734 735 paint.getTextBounds(referenceChar, 0, 1, mTextBounds); 736 final float width = mTextBounds.width(); 737 sTextWidthCache.put(key, width); 738 return width; 739 } 740 741 public float getLabelWidth(String label, Paint paint) { 742 paint.getTextBounds(label.toString(), 0, label.length(), mTextBounds); 743 return mTextBounds.width(); 744 } 745 746 protected static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 747 int height) { 748 canvas.translate(x, y); 749 icon.setBounds(0, 0, width, height); 750 icon.draw(canvas); 751 canvas.translate(-x, -y); 752 } 753 754 private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, 755 Paint paint) { 756 paint.setStyle(Paint.Style.STROKE); 757 paint.setStrokeWidth(1.0f); 758 paint.setColor(color); 759 canvas.drawLine(0, y, w, y, paint); 760 } 761 762 private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) { 763 paint.setStyle(Paint.Style.STROKE); 764 paint.setStrokeWidth(1.0f); 765 paint.setColor(color); 766 canvas.drawLine(x, 0, x, h, paint); 767 } 768 769 private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, 770 Paint paint) { 771 paint.setStyle(Paint.Style.STROKE); 772 paint.setStrokeWidth(1.0f); 773 paint.setColor(color); 774 canvas.translate(x, y); 775 canvas.drawRect(0, 0, w, h, paint); 776 canvas.translate(-x, -y); 777 } 778 779 public Paint newDefaultLabelPaint() { 780 final Paint paint = new Paint(); 781 paint.setAntiAlias(true); 782 paint.setTypeface(mKeyDrawParams.mTypeface); 783 paint.setTextSize(mKeyDrawParams.mLabelSize); 784 return paint; 785 } 786 787 public void cancelAllMessages() { 788 mDrawingHandler.cancelAllMessages(); 789 if (mPreviewPlacerView != null) { 790 mPreviewPlacerView.cancelAllMessages(); 791 } 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[] viewOrigin = new int[2]; 836 getLocationInWindow(viewOrigin); 837 mPreviewPlacerView.setOrigin(viewOrigin[0], viewOrigin[1]); 838 final View rootView = getRootView(); 839 if (rootView == null) { 840 Log.w(TAG, "Cannot find root view"); 841 return; 842 } 843 final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content); 844 // Note: It'd be very weird if we get null by android.R.id.content. 845 if (windowContentView == null) { 846 Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView"); 847 } else { 848 windowContentView.addView(mPreviewPlacerView); 849 } 850 } 851 852 public void showGestureFloatingPreviewText(String gestureFloatingPreviewText) { 853 locatePreviewPlacerView(); 854 mPreviewPlacerView.setGestureFloatingPreviewText(gestureFloatingPreviewText); 855 } 856 857 public void dismissGestureFloatingPreviewText() { 858 locatePreviewPlacerView(); 859 mPreviewPlacerView.dismissGestureFloatingPreviewText(); 860 } 861 862 @Override 863 public void showGesturePreviewTrail(final PointerTracker tracker) { 864 locatePreviewPlacerView(); 865 mPreviewPlacerView.invalidatePointer(tracker); 866 } 867 868 @SuppressWarnings("deprecation") // setBackgroundDrawable is replaced by setBackground in API16 869 @Override 870 public void showKeyPreview(final PointerTracker tracker) { 871 final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams; 872 if (!mShowKeyPreviewPopup) { 873 previewParams.mPreviewVisibleOffset = -mKeyboard.mVerticalGap; 874 return; 875 } 876 877 final TextView previewText = getKeyPreviewText(tracker.mPointerId); 878 // If the key preview has no parent view yet, add it to the ViewGroup which can place 879 // key preview absolutely in SoftInputWindow. 880 if (previewText.getParent() == null) { 881 addKeyPreview(previewText); 882 } 883 884 mDrawingHandler.cancelDismissKeyPreview(tracker); 885 final Key key = tracker.getKey(); 886 // If key is invalid or IME is already closed, we must not show key preview. 887 // Trying to show key preview while root window is closed causes 888 // WindowManager.BadTokenException. 889 if (key == null) 890 return; 891 892 final KeyDrawParams drawParams = mKeyDrawParams; 893 final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel; 894 // What we show as preview should match what we show on a key top in onDraw(). 895 if (label != null) { 896 // TODO Should take care of temporaryShiftLabel here. 897 previewText.setCompoundDrawables(null, null, null, null); 898 if (StringUtils.codePointCount(label) > 1) { 899 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize); 900 previewText.setTypeface(Typeface.DEFAULT_BOLD); 901 } else { 902 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize); 903 previewText.setTypeface(key.selectTypeface(drawParams)); 904 } 905 previewText.setText(label); 906 } else { 907 previewText.setCompoundDrawables(null, null, null, 908 key.getPreviewIcon(mKeyboard.mIconsSet)); 909 previewText.setText(null); 910 } 911 previewText.setBackgroundDrawable(mPreviewBackground); 912 913 previewText.measure( 914 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 915 final int keyDrawWidth = key.getDrawWidth(); 916 final int previewWidth = previewText.getMeasuredWidth(); 917 final int previewHeight = mPreviewHeight; 918 // The width and height of visible part of the key preview background. The content marker 919 // of the background 9-patch have to cover the visible part of the background. 920 previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft() 921 - previewText.getPaddingRight(); 922 previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop() 923 - previewText.getPaddingBottom(); 924 // The distance between the top edge of the parent key and the bottom of the visible part 925 // of the key preview background. 926 previewParams.mPreviewVisibleOffset = mPreviewOffset - previewText.getPaddingBottom(); 927 getLocationInWindow(previewParams.mCoordinates); 928 // The key preview is horizontally aligned with the center of the visible part of the 929 // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and 930 // the left/right background is used if such background is specified. 931 int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 932 + previewParams.mCoordinates[0]; 933 if (previewX < 0) { 934 previewX = 0; 935 if (mPreviewLeftBackground != null) { 936 previewText.setBackgroundDrawable(mPreviewLeftBackground); 937 } 938 } else if (previewX > getWidth() - previewWidth) { 939 previewX = getWidth() - previewWidth; 940 if (mPreviewRightBackground != null) { 941 previewText.setBackgroundDrawable(mPreviewRightBackground); 942 } 943 } 944 // The key preview is placed vertically above the top edge of the parent key with an 945 // arbitrary offset. 946 final int previewY = key.mY - previewHeight + mPreviewOffset 947 + previewParams.mCoordinates[1]; 948 949 // Set the preview background state 950 previewText.getBackground().setState( 951 key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 952 previewText.setTextColor(drawParams.mPreviewTextColor); 953 ViewLayoutUtils.placeViewAt( 954 previewText, previewX, previewY, previewWidth, previewHeight); 955 previewText.setVisibility(VISIBLE); 956 } 957 958 /** 959 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 960 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 961 * draws the cached buffer. 962 * @see #invalidateKey(Key) 963 */ 964 public void invalidateAllKeys() { 965 mInvalidatedKeys.clear(); 966 mInvalidateAllKeys = true; 967 invalidate(); 968 } 969 970 /** 971 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 972 * one key is changing it's content. Any changes that affect the position or size of the key 973 * may not be honored. 974 * @param key key in the attached {@link Keyboard}. 975 * @see #invalidateAllKeys 976 */ 977 @Override 978 public void invalidateKey(Key key) { 979 if (mInvalidateAllKeys) return; 980 if (key == null) return; 981 mInvalidatedKeys.add(key); 982 final int x = key.mX + getPaddingLeft(); 983 final int y = key.mY + getPaddingTop(); 984 invalidate(x, y, x + key.mWidth, y + key.mHeight); 985 } 986 987 public void closing() { 988 dismissAllKeyPreviews(); 989 cancelAllMessages(); 990 991 mInvalidateAllKeys = true; 992 requestLayout(); 993 } 994 995 @Override 996 public boolean dismissMoreKeysPanel() { 997 return false; 998 } 999 1000 public void purgeKeyboardAndClosing() { 1001 mKeyboard = null; 1002 closing(); 1003 } 1004 1005 @Override 1006 protected void onDetachedFromWindow() { 1007 super.onDetachedFromWindow(); 1008 closing(); 1009 mPreviewPlacerView.removeAllViews(); 1010 freeOffscreenBuffer(); 1011 } 1012} 1013