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