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.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Align; 27 import android.graphics.PorterDuff; 28 import android.graphics.Rect; 29 import android.graphics.Region.Op; 30 import android.graphics.Typeface; 31 import android.graphics.drawable.Drawable; 32 import android.os.Message; 33 import android.util.AttributeSet; 34 import android.util.TypedValue; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.RelativeLayout; 39 import android.widget.TextView; 40 41 import com.android.inputmethod.compat.FrameLayoutCompatUtils; 42 import com.android.inputmethod.latin.LatinImeLogger; 43 import com.android.inputmethod.latin.R; 44 import com.android.inputmethod.latin.StaticInnerHandlerWrapper; 45 46 import java.util.HashMap; 47 48 /** 49 * A view that renders a virtual {@link Keyboard}. 50 * 51 * @attr ref R.styleable#KeyboardView_backgroundDimAmount 52 * @attr ref R.styleable#KeyboardView_keyBackground 53 * @attr ref R.styleable#KeyboardView_keyLetterRatio 54 * @attr ref R.styleable#KeyboardView_keyLargeLetterRatio 55 * @attr ref R.styleable#KeyboardView_keyLabelRatio 56 * @attr ref R.styleable#KeyboardView_keyHintLetterRatio 57 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterRatio 58 * @attr ref R.styleable#KeyboardView_keyHintLabelRatio 59 * @attr ref R.styleable#KeyboardView_keyLabelHorizontalPadding 60 * @attr ref R.styleable#KeyboardView_keyHintLetterPadding 61 * @attr ref R.styleable#KeyboardView_keyPopupHintLetterPadding 62 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterPadding 63 * @attr ref R.styleable#KeyboardView_keyTextStyle 64 * @attr ref R.styleable#KeyboardView_keyPreviewLayout 65 * @attr ref R.styleable#KeyboardView_keyPreviewTextRatio 66 * @attr ref R.styleable#KeyboardView_keyPreviewOffset 67 * @attr ref R.styleable#KeyboardView_keyPreviewHeight 68 * @attr ref R.styleable#KeyboardView_keyTextColor 69 * @attr ref R.styleable#KeyboardView_keyTextColorDisabled 70 * @attr ref R.styleable#KeyboardView_keyHintLetterColor 71 * @attr ref R.styleable#KeyboardView_keyHintLabelColor 72 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterInactivatedColor 73 * @attr ref R.styleable#KeyboardView_keyUppercaseLetterActivatedColor 74 * @attr ref R.styleable#KeyboardView_shadowColor 75 * @attr ref R.styleable#KeyboardView_shadowRadius 76 */ 77 public class KeyboardView extends View implements PointerTracker.DrawingProxy { 78 // Miscellaneous constants 79 private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable }; 80 81 // XML attributes 82 protected final float mVerticalCorrection; 83 protected final int mMoreKeysLayout; 84 private final float mBackgroundDimAmount; 85 86 // HORIZONTAL ELLIPSIS "...", character for popup hint. 87 private static final String POPUP_HINT_CHAR = "\u2026"; 88 89 // Margin between the label and the icon on a key that has both of them. 90 // Specified by the fraction of the key width. 91 // TODO: Use resource parameter for this value. 92 private static final float LABEL_ICON_MARGIN = 0.05f; 93 94 // The maximum key label width in the proportion to the key width. 95 private static final float MAX_LABEL_RATIO = 0.90f; 96 97 // Main keyboard 98 private Keyboard mKeyboard; 99 private final KeyDrawParams mKeyDrawParams; 100 101 // Key preview 102 private final int mKeyPreviewLayoutId; 103 protected final KeyPreviewDrawParams mKeyPreviewDrawParams; 104 private boolean mShowKeyPreviewPopup = true; 105 private final int mDelayBeforePreview; 106 private int mDelayAfterPreview; 107 private ViewGroup mPreviewPlacer; 108 109 // Drawing 110 /** True if the entire keyboard needs to be dimmed. */ 111 private boolean mNeedsToDimBackground; 112 /** Whether the keyboard bitmap buffer needs to be redrawn before it's blitted. **/ 113 private boolean mBufferNeedsUpdate; 114 /** The dirty region in the keyboard bitmap */ 115 private final Rect mDirtyRect = new Rect(); 116 /** The key to invalidate. */ 117 private Key mInvalidatedKey; 118 /** The dirty region for single key drawing */ 119 private final Rect mInvalidatedKeyRect = new Rect(); 120 /** The keyboard bitmap buffer for faster updates */ 121 private Bitmap mBuffer; 122 /** The canvas for the above mutable keyboard bitmap */ 123 private Canvas mCanvas; 124 private final Paint mPaint = new Paint(); 125 // This map caches key label text height in pixel as value and key label text size as map key. 126 private static final HashMap<Integer, Float> sTextHeightCache = 127 new HashMap<Integer, Float>(); 128 // This map caches key label text width in pixel as value and key label text size as map key. 129 private static final HashMap<Integer, Float> sTextWidthCache = 130 new HashMap<Integer, Float>(); 131 private static final char[] KEY_LABEL_REFERENCE_CHAR = { 'M' }; 132 private static final char[] KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR = { '8' }; 133 134 private final DrawingHandler mDrawingHandler = new DrawingHandler(this); 135 136 public static class DrawingHandler extends StaticInnerHandlerWrapper<KeyboardView> { 137 private static final int MSG_SHOW_KEY_PREVIEW = 1; 138 private static final int MSG_DISMISS_KEY_PREVIEW = 2; 139 DrawingHandler(KeyboardView outerInstance)140 public DrawingHandler(KeyboardView outerInstance) { 141 super(outerInstance); 142 } 143 144 @Override handleMessage(Message msg)145 public void handleMessage(Message msg) { 146 final KeyboardView keyboardView = getOuterInstance(); 147 if (keyboardView == null) return; 148 final PointerTracker tracker = (PointerTracker) msg.obj; 149 switch (msg.what) { 150 case MSG_SHOW_KEY_PREVIEW: 151 keyboardView.showKey(msg.arg1, tracker); 152 break; 153 case MSG_DISMISS_KEY_PREVIEW: 154 tracker.getKeyPreviewText().setVisibility(View.INVISIBLE); 155 break; 156 } 157 } 158 showKeyPreview(long delay, int keyIndex, PointerTracker tracker)159 public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) { 160 removeMessages(MSG_SHOW_KEY_PREVIEW); 161 final KeyboardView keyboardView = getOuterInstance(); 162 if (keyboardView == null) return; 163 if (tracker.getKeyPreviewText().getVisibility() == VISIBLE || delay == 0) { 164 // Show right away, if it's already visible and finger is moving around 165 keyboardView.showKey(keyIndex, tracker); 166 } else { 167 sendMessageDelayed( 168 obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay); 169 } 170 } 171 cancelShowKeyPreview(PointerTracker tracker)172 public void cancelShowKeyPreview(PointerTracker tracker) { 173 removeMessages(MSG_SHOW_KEY_PREVIEW, tracker); 174 } 175 cancelAllShowKeyPreviews()176 public void cancelAllShowKeyPreviews() { 177 removeMessages(MSG_SHOW_KEY_PREVIEW); 178 } 179 dismissKeyPreview(long delay, PointerTracker tracker)180 public void dismissKeyPreview(long delay, PointerTracker tracker) { 181 sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay); 182 } 183 cancelDismissKeyPreview(PointerTracker tracker)184 public void cancelDismissKeyPreview(PointerTracker tracker) { 185 removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker); 186 } 187 cancelAllDismissKeyPreviews()188 public void cancelAllDismissKeyPreviews() { 189 removeMessages(MSG_DISMISS_KEY_PREVIEW); 190 } 191 cancelAllMessages()192 public void cancelAllMessages() { 193 cancelAllShowKeyPreviews(); 194 cancelAllDismissKeyPreviews(); 195 } 196 } 197 198 private static class KeyDrawParams { 199 // XML attributes 200 public final int mKeyTextColor; 201 public final int mKeyTextInactivatedColor; 202 public final Typeface mKeyTextStyle; 203 public final float mKeyLabelHorizontalPadding; 204 public final float mKeyHintLetterPadding; 205 public final float mKeyPopupHintLetterPadding; 206 public final float mKeyUppercaseLetterPadding; 207 public final int mShadowColor; 208 public final float mShadowRadius; 209 public final Drawable mKeyBackground; 210 public final int mKeyHintLetterColor; 211 public final int mKeyHintLabelColor; 212 public final int mKeyUppercaseLetterInactivatedColor; 213 public final int mKeyUppercaseLetterActivatedColor; 214 215 private final float mKeyLetterRatio; 216 private final float mKeyLargeLetterRatio; 217 private final float mKeyLabelRatio; 218 private final float mKeyHintLetterRatio; 219 private final float mKeyUppercaseLetterRatio; 220 private final float mKeyHintLabelRatio; 221 private static final float UNDEFINED_RATIO = -1.0f; 222 223 public final Rect mPadding = new Rect(); 224 public int mKeyLetterSize; 225 public int mKeyLargeLetterSize; 226 public int mKeyLabelSize; 227 public int mKeyHintLetterSize; 228 public int mKeyUppercaseLetterSize; 229 public int mKeyHintLabelSize; 230 KeyDrawParams(TypedArray a)231 public KeyDrawParams(TypedArray a) { 232 mKeyBackground = a.getDrawable(R.styleable.KeyboardView_keyBackground); 233 if (a.hasValue(R.styleable.KeyboardView_keyLetterSize)) { 234 mKeyLetterRatio = UNDEFINED_RATIO; 235 mKeyLetterSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLetterSize, 0); 236 } else { 237 mKeyLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLetterRatio); 238 } 239 if (a.hasValue(R.styleable.KeyboardView_keyLabelSize)) { 240 mKeyLabelRatio = UNDEFINED_RATIO; 241 mKeyLabelSize = a.getDimensionPixelSize(R.styleable.KeyboardView_keyLabelSize, 0); 242 } else { 243 mKeyLabelRatio = getRatio(a, R.styleable.KeyboardView_keyLabelRatio); 244 } 245 mKeyLargeLetterRatio = getRatio(a, R.styleable.KeyboardView_keyLargeLetterRatio); 246 mKeyHintLetterRatio = getRatio(a, R.styleable.KeyboardView_keyHintLetterRatio); 247 mKeyUppercaseLetterRatio = getRatio(a, 248 R.styleable.KeyboardView_keyUppercaseLetterRatio); 249 mKeyHintLabelRatio = getRatio(a, R.styleable.KeyboardView_keyHintLabelRatio); 250 mKeyLabelHorizontalPadding = a.getDimension( 251 R.styleable.KeyboardView_keyLabelHorizontalPadding, 0); 252 mKeyHintLetterPadding = a.getDimension( 253 R.styleable.KeyboardView_keyHintLetterPadding, 0); 254 mKeyPopupHintLetterPadding = a.getDimension( 255 R.styleable.KeyboardView_keyPopupHintLetterPadding, 0); 256 mKeyUppercaseLetterPadding = a.getDimension( 257 R.styleable.KeyboardView_keyUppercaseLetterPadding, 0); 258 mKeyTextColor = a.getColor(R.styleable.KeyboardView_keyTextColor, 0xFF000000); 259 mKeyTextInactivatedColor = a.getColor( 260 R.styleable.KeyboardView_keyTextInactivatedColor, 0xFF000000); 261 mKeyHintLetterColor = a.getColor(R.styleable.KeyboardView_keyHintLetterColor, 0); 262 mKeyHintLabelColor = a.getColor(R.styleable.KeyboardView_keyHintLabelColor, 0); 263 mKeyUppercaseLetterInactivatedColor = a.getColor( 264 R.styleable.KeyboardView_keyUppercaseLetterInactivatedColor, 0); 265 mKeyUppercaseLetterActivatedColor = a.getColor( 266 R.styleable.KeyboardView_keyUppercaseLetterActivatedColor, 0); 267 mKeyTextStyle = Typeface.defaultFromStyle( 268 a.getInt(R.styleable.KeyboardView_keyTextStyle, Typeface.NORMAL)); 269 mShadowColor = a.getColor(R.styleable.KeyboardView_shadowColor, 0); 270 mShadowRadius = a.getFloat(R.styleable.KeyboardView_shadowRadius, 0f); 271 272 mKeyBackground.getPadding(mPadding); 273 } 274 updateKeyHeight(int keyHeight)275 public void updateKeyHeight(int keyHeight) { 276 if (mKeyLetterRatio >= 0.0f) 277 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 278 if (mKeyLabelRatio >= 0.0f) 279 mKeyLabelSize = (int)(keyHeight * mKeyLabelRatio); 280 mKeyLargeLetterSize = (int)(keyHeight * mKeyLargeLetterRatio); 281 mKeyHintLetterSize = (int)(keyHeight * mKeyHintLetterRatio); 282 mKeyUppercaseLetterSize = (int)(keyHeight * mKeyUppercaseLetterRatio); 283 mKeyHintLabelSize = (int)(keyHeight * mKeyHintLabelRatio); 284 } 285 } 286 287 protected static class KeyPreviewDrawParams { 288 // XML attributes. 289 public final Drawable mPreviewBackground; 290 public final Drawable mPreviewLeftBackground; 291 public final Drawable mPreviewRightBackground; 292 public final int mPreviewBackgroundWidth; 293 public final int mPreviewBackgroundHeight; 294 public final int mPreviewTextColor; 295 public final int mPreviewOffset; 296 public final int mPreviewHeight; 297 public final Typeface mKeyTextStyle; 298 299 private final float mPreviewTextRatio; 300 private final float mKeyLetterRatio; 301 302 public int mPreviewTextSize; 303 public int mKeyLetterSize; 304 public final int[] mCoordinates = new int[2]; 305 306 private static final int PREVIEW_ALPHA = 240; 307 KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams)308 public KeyPreviewDrawParams(TypedArray a, KeyDrawParams keyDrawParams) { 309 mPreviewBackground = a.getDrawable(R.styleable.KeyboardView_keyPreviewBackground); 310 mPreviewLeftBackground = a.getDrawable( 311 R.styleable.KeyboardView_keyPreviewLeftBackground); 312 mPreviewRightBackground = a.getDrawable( 313 R.styleable.KeyboardView_keyPreviewRightBackground); 314 setAlpha(mPreviewBackground, PREVIEW_ALPHA); 315 setAlpha(mPreviewLeftBackground, PREVIEW_ALPHA); 316 setAlpha(mPreviewRightBackground, PREVIEW_ALPHA); 317 mPreviewBackgroundWidth = a.getDimensionPixelSize( 318 R.styleable.KeyboardView_keyPreviewBackgroundWidth, 0); 319 mPreviewBackgroundHeight = a.getDimensionPixelSize( 320 R.styleable.KeyboardView_keyPreviewBackgroundHeight, 0); 321 mPreviewOffset = a.getDimensionPixelOffset( 322 R.styleable.KeyboardView_keyPreviewOffset, 0); 323 mPreviewHeight = a.getDimensionPixelSize( 324 R.styleable.KeyboardView_keyPreviewHeight, 80); 325 mPreviewTextRatio = getRatio(a, R.styleable.KeyboardView_keyPreviewTextRatio); 326 mPreviewTextColor = a.getColor(R.styleable.KeyboardView_keyPreviewTextColor, 0); 327 328 mKeyLetterRatio = keyDrawParams.mKeyLetterRatio; 329 mKeyTextStyle = keyDrawParams.mKeyTextStyle; 330 } 331 updateKeyHeight(int keyHeight)332 public void updateKeyHeight(int keyHeight) { 333 mPreviewTextSize = (int)(keyHeight * mPreviewTextRatio); 334 mKeyLetterSize = (int)(keyHeight * mKeyLetterRatio); 335 } 336 setAlpha(Drawable drawable, int alpha)337 private static void setAlpha(Drawable drawable, int alpha) { 338 if (drawable == null) 339 return; 340 drawable.setAlpha(alpha); 341 } 342 } 343 KeyboardView(Context context, AttributeSet attrs)344 public KeyboardView(Context context, AttributeSet attrs) { 345 this(context, attrs, R.attr.keyboardViewStyle); 346 } 347 KeyboardView(Context context, AttributeSet attrs, int defStyle)348 public KeyboardView(Context context, AttributeSet attrs, int defStyle) { 349 super(context, attrs, defStyle); 350 351 final TypedArray a = context.obtainStyledAttributes( 352 attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView); 353 354 mKeyDrawParams = new KeyDrawParams(a); 355 mKeyPreviewDrawParams = new KeyPreviewDrawParams(a, mKeyDrawParams); 356 mKeyPreviewLayoutId = a.getResourceId(R.styleable.KeyboardView_keyPreviewLayout, 0); 357 if (mKeyPreviewLayoutId == 0) { 358 mShowKeyPreviewPopup = false; 359 } 360 mVerticalCorrection = a.getDimensionPixelOffset( 361 R.styleable.KeyboardView_verticalCorrection, 0); 362 mMoreKeysLayout = a.getResourceId(R.styleable.KeyboardView_moreKeysLayout, 0); 363 mBackgroundDimAmount = a.getFloat(R.styleable.KeyboardView_backgroundDimAmount, 0.5f); 364 a.recycle(); 365 366 final Resources res = getResources(); 367 368 mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview); 369 mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview); 370 371 mPaint.setAntiAlias(true); 372 mPaint.setTextAlign(Align.CENTER); 373 mPaint.setAlpha(255); 374 } 375 376 // Read fraction value in TypedArray as float. getRatio(TypedArray a, int index)377 private static float getRatio(TypedArray a, int index) { 378 return a.getFraction(index, 1000, 1000, 1) / 1000.0f; 379 } 380 381 /** 382 * Attaches a keyboard to this view. The keyboard can be switched at any time and the 383 * view will re-layout itself to accommodate the keyboard. 384 * @see Keyboard 385 * @see #getKeyboard() 386 * @param keyboard the keyboard to display in this view 387 */ setKeyboard(Keyboard keyboard)388 public void setKeyboard(Keyboard keyboard) { 389 // Remove any pending dismissing preview 390 mDrawingHandler.cancelAllShowKeyPreviews(); 391 if (mKeyboard != null) { 392 PointerTracker.dismissAllKeyPreviews(); 393 } 394 mKeyboard = keyboard; 395 LatinImeLogger.onSetKeyboard(keyboard); 396 requestLayout(); 397 mDirtyRect.set(0, 0, getWidth(), getHeight()); 398 mBufferNeedsUpdate = true; 399 invalidateAllKeys(); 400 final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap; 401 mKeyDrawParams.updateKeyHeight(keyHeight); 402 mKeyPreviewDrawParams.updateKeyHeight(keyHeight); 403 } 404 405 /** 406 * Returns the current keyboard being displayed by this view. 407 * @return the currently attached keyboard 408 * @see #setKeyboard(Keyboard) 409 */ getKeyboard()410 public Keyboard getKeyboard() { 411 return mKeyboard; 412 } 413 414 /** 415 * Enables or disables the key feedback popup. This is a popup that shows a magnified 416 * version of the depressed key. By default the preview is enabled. 417 * @param previewEnabled whether or not to enable the key feedback preview 418 * @param delay the delay after which the preview is dismissed 419 * @see #isKeyPreviewPopupEnabled() 420 */ setKeyPreviewPopupEnabled(boolean previewEnabled, int delay)421 public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) { 422 mShowKeyPreviewPopup = previewEnabled; 423 mDelayAfterPreview = delay; 424 } 425 426 /** 427 * Returns the enabled state of the key feedback preview 428 * @return whether or not the key feedback preview is enabled 429 * @see #setKeyPreviewPopupEnabled(boolean, int) 430 */ isKeyPreviewPopupEnabled()431 public boolean isKeyPreviewPopupEnabled() { 432 return mShowKeyPreviewPopup; 433 } 434 435 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)436 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 437 if (mKeyboard != null) { 438 // The main keyboard expands to the display width. 439 final int height = mKeyboard.mOccupiedHeight + getPaddingTop() + getPaddingBottom(); 440 setMeasuredDimension(widthMeasureSpec, height); 441 } else { 442 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 443 } 444 } 445 446 @Override onDraw(Canvas canvas)447 public void onDraw(Canvas canvas) { 448 super.onDraw(canvas); 449 if (mBufferNeedsUpdate || mBuffer == null) { 450 mBufferNeedsUpdate = false; 451 onBufferDraw(); 452 } 453 canvas.drawBitmap(mBuffer, 0, 0, null); 454 } 455 onBufferDraw()456 private void onBufferDraw() { 457 final int width = getWidth(); 458 final int height = getHeight(); 459 if (width == 0 || height == 0) 460 return; 461 if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) { 462 if (mBuffer != null) 463 mBuffer.recycle(); 464 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 465 mDirtyRect.union(0, 0, width, height); 466 if (mCanvas != null) { 467 mCanvas.setBitmap(mBuffer); 468 } else { 469 mCanvas = new Canvas(mBuffer); 470 } 471 } 472 final Canvas canvas = mCanvas; 473 canvas.clipRect(mDirtyRect, Op.REPLACE); 474 canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); 475 476 if (mKeyboard == null) return; 477 478 final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase(); 479 final KeyDrawParams params = mKeyDrawParams; 480 if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) { 481 // Draw a single key. 482 final int keyDrawX = mInvalidatedKey.mX + mInvalidatedKey.mVisualInsetsLeft 483 + getPaddingLeft(); 484 final int keyDrawY = mInvalidatedKey.mY + getPaddingTop(); 485 canvas.translate(keyDrawX, keyDrawY); 486 onBufferDrawKey(mInvalidatedKey, mKeyboard, canvas, mPaint, params, 487 isManualTemporaryUpperCase); 488 canvas.translate(-keyDrawX, -keyDrawY); 489 } else { 490 // Draw all keys. 491 for (final Key key : mKeyboard.mKeys) { 492 final int keyDrawX = key.mX + key.mVisualInsetsLeft + getPaddingLeft(); 493 final int keyDrawY = key.mY + getPaddingTop(); 494 canvas.translate(keyDrawX, keyDrawY); 495 onBufferDrawKey(key, mKeyboard, canvas, mPaint, params, isManualTemporaryUpperCase); 496 canvas.translate(-keyDrawX, -keyDrawY); 497 } 498 } 499 500 // Overlay a dark rectangle to dim the entire keyboard 501 if (mNeedsToDimBackground) { 502 mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24); 503 canvas.drawRect(0, 0, width, height, mPaint); 504 } 505 506 mInvalidatedKey = null; 507 mDirtyRect.setEmpty(); 508 } 509 dimEntireKeyboard(boolean dimmed)510 public void dimEntireKeyboard(boolean dimmed) { 511 final boolean needsRedrawing = mNeedsToDimBackground != dimmed; 512 mNeedsToDimBackground = dimmed; 513 if (needsRedrawing) { 514 invalidateAllKeys(); 515 } 516 } 517 onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas, Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase)518 private static void onBufferDrawKey(final Key key, final Keyboard keyboard, final Canvas canvas, 519 Paint paint, KeyDrawParams params, boolean isManualTemporaryUpperCase) { 520 final boolean debugShowAlign = LatinImeLogger.sVISUALDEBUG; 521 // Draw key background. 522 if (!key.isSpacer()) { 523 final int bgWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight 524 + params.mPadding.left + params.mPadding.right; 525 final int bgHeight = key.mHeight + params.mPadding.top + params.mPadding.bottom; 526 final int bgX = -params.mPadding.left; 527 final int bgY = -params.mPadding.top; 528 final int[] drawableState = key.getCurrentDrawableState(); 529 final Drawable background = params.mKeyBackground; 530 background.setState(drawableState); 531 final Rect bounds = background.getBounds(); 532 if (bgWidth != bounds.right || bgHeight != bounds.bottom) { 533 background.setBounds(0, 0, bgWidth, bgHeight); 534 } 535 canvas.translate(bgX, bgY); 536 background.draw(canvas); 537 if (debugShowAlign) { 538 drawRectangle(canvas, 0, 0, bgWidth, bgHeight, 0x80c00000, new Paint()); 539 } 540 canvas.translate(-bgX, -bgY); 541 } 542 543 // Draw key top visuals. 544 final int keyWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 545 final int keyHeight = key.mHeight; 546 final float centerX = keyWidth * 0.5f; 547 final float centerY = keyHeight * 0.5f; 548 549 if (debugShowAlign) { 550 drawRectangle(canvas, 0, 0, keyWidth, keyHeight, 0x800000c0, new Paint()); 551 } 552 553 // Draw key label. 554 final Drawable icon = key.getIcon(); 555 float positionX = centerX; 556 if (key.mLabel != null) { 557 // Switch the character to uppercase if shift is pressed 558 final CharSequence label = keyboard.adjustLabelCase(key.mLabel); 559 // For characters, use large font. For labels like "Done", use smaller font. 560 paint.setTypeface(key.selectTypeface(params.mKeyTextStyle)); 561 final int labelSize = key.selectTextSize(params.mKeyLetterSize, 562 params.mKeyLargeLetterSize, params.mKeyLabelSize, params.mKeyHintLabelSize); 563 paint.setTextSize(labelSize); 564 final float labelCharHeight = getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint); 565 final float labelCharWidth = getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint); 566 567 // Vertical label text alignment. 568 final float baseline = centerY + labelCharHeight / 2; 569 570 // Horizontal label text alignment 571 float labelWidth = 0; 572 if (key.isAlignLeft()) { 573 positionX = (int)params.mKeyLabelHorizontalPadding; 574 paint.setTextAlign(Align.LEFT); 575 } else if (key.isAlignRight()) { 576 positionX = keyWidth - (int)params.mKeyLabelHorizontalPadding; 577 paint.setTextAlign(Align.RIGHT); 578 } else if (key.isAlignLeftOfCenter()) { 579 // TODO: Parameterise this? 580 positionX = centerX - labelCharWidth * 7 / 4; 581 paint.setTextAlign(Align.LEFT); 582 } else if (key.hasLabelWithIconLeft() && icon != null) { 583 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 584 + LABEL_ICON_MARGIN * keyWidth; 585 positionX = centerX + labelWidth / 2; 586 paint.setTextAlign(Align.RIGHT); 587 } else if (key.hasLabelWithIconRight() && icon != null) { 588 labelWidth = getLabelWidth(label, paint) + icon.getIntrinsicWidth() 589 + LABEL_ICON_MARGIN * keyWidth; 590 positionX = centerX - labelWidth / 2; 591 paint.setTextAlign(Align.LEFT); 592 } else { 593 positionX = centerX; 594 paint.setTextAlign(Align.CENTER); 595 } 596 if (key.needsXScale()) { 597 paint.setTextScaleX( 598 Math.min(1.0f, (keyWidth * MAX_LABEL_RATIO) / getLabelWidth(label, paint))); 599 } 600 601 if (key.hasUppercaseLetter() && isManualTemporaryUpperCase) { 602 paint.setColor(params.mKeyTextInactivatedColor); 603 } else { 604 paint.setColor(params.mKeyTextColor); 605 } 606 if (key.isEnabled()) { 607 // Set a drop shadow for the text 608 paint.setShadowLayer(params.mShadowRadius, 0, 0, params.mShadowColor); 609 } else { 610 // Make label invisible 611 paint.setColor(Color.TRANSPARENT); 612 } 613 canvas.drawText(label, 0, label.length(), positionX, baseline, paint); 614 // Turn off drop shadow and reset x-scale. 615 paint.setShadowLayer(0, 0, 0, 0); 616 paint.setTextScaleX(1.0f); 617 618 if (icon != null) { 619 final int iconWidth = icon.getIntrinsicWidth(); 620 final int iconHeight = icon.getIntrinsicHeight(); 621 final int iconY = (keyHeight - iconHeight) / 2; 622 if (key.hasLabelWithIconLeft()) { 623 final int iconX = (int)(centerX - labelWidth / 2); 624 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 625 } else if (key.hasLabelWithIconRight()) { 626 final int iconX = (int)(centerX + labelWidth / 2 - iconWidth); 627 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 628 } 629 } 630 631 if (debugShowAlign) { 632 final Paint line = new Paint(); 633 drawHorizontalLine(canvas, baseline, keyWidth, 0xc0008000, line); 634 drawVerticalLine(canvas, positionX, keyHeight, 0xc0800080, line); 635 } 636 } 637 638 // Draw hint label. 639 if (key.mHintLabel != null) { 640 final CharSequence hint = key.mHintLabel; 641 final int hintColor; 642 final int hintSize; 643 if (key.hasHintLabel()) { 644 hintColor = params.mKeyHintLabelColor; 645 hintSize = params.mKeyHintLabelSize; 646 paint.setTypeface(Typeface.DEFAULT); 647 } else if (key.hasUppercaseLetter()) { 648 hintColor = isManualTemporaryUpperCase 649 ? params.mKeyUppercaseLetterActivatedColor 650 : params.mKeyUppercaseLetterInactivatedColor; 651 hintSize = params.mKeyUppercaseLetterSize; 652 } else { // key.hasHintLetter() 653 hintColor = params.mKeyHintLetterColor; 654 hintSize = params.mKeyHintLetterSize; 655 } 656 paint.setColor(hintColor); 657 paint.setTextSize(hintSize); 658 final float hintX, hintY; 659 if (key.hasHintLabel()) { 660 // The hint label is placed just right of the key label. Used mainly on 661 // "phone number" layout. 662 // TODO: Generalize the following calculations. 663 hintX = positionX + getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) * 2; 664 hintY = centerY + getCharHeight(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 665 paint.setTextAlign(Align.LEFT); 666 } else if (key.hasUppercaseLetter()) { 667 // The hint label is placed at top-right corner of the key. Used mainly on tablet. 668 hintX = keyWidth - params.mKeyUppercaseLetterPadding 669 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 670 hintY = -paint.ascent(); 671 paint.setTextAlign(Align.CENTER); 672 } else { // key.hasHintLetter() 673 // The hint label is placed at top-right corner of the key. Used mainly on phone. 674 hintX = keyWidth - params.mKeyHintLetterPadding 675 - getCharWidth(KEY_NUMERIC_HINT_LABEL_REFERENCE_CHAR, paint) / 2; 676 hintY = -paint.ascent(); 677 paint.setTextAlign(Align.CENTER); 678 } 679 canvas.drawText(hint, 0, hint.length(), hintX, hintY, paint); 680 681 if (debugShowAlign) { 682 final Paint line = new Paint(); 683 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 684 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 685 } 686 } 687 688 // Draw key icon. 689 if (key.mLabel == null && icon != null) { 690 final int iconWidth = icon.getIntrinsicWidth(); 691 final int iconHeight = icon.getIntrinsicHeight(); 692 final int iconX, alignX; 693 final int iconY = (keyHeight - iconHeight) / 2; 694 if (key.isAlignLeft()) { 695 iconX = (int)params.mKeyLabelHorizontalPadding; 696 alignX = iconX; 697 } else if (key.isAlignRight()) { 698 iconX = keyWidth - (int)params.mKeyLabelHorizontalPadding - iconWidth; 699 alignX = iconX + iconWidth; 700 } else { // Align center 701 iconX = (keyWidth - iconWidth) / 2; 702 alignX = iconX + iconWidth / 2; 703 } 704 drawIcon(canvas, icon, iconX, iconY, iconWidth, iconHeight); 705 706 if (debugShowAlign) { 707 final Paint line = new Paint(); 708 drawVerticalLine(canvas, alignX, keyHeight, 0xc0800080, line); 709 drawRectangle(canvas, iconX, iconY, iconWidth, iconHeight, 0x80c00000, line); 710 } 711 } 712 713 // Draw popup hint "..." at the bottom right corner of the key. 714 if ((key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) 715 || key.needsSpecialPopupHint()) { 716 paint.setTextSize(params.mKeyHintLetterSize); 717 paint.setColor(params.mKeyHintLabelColor); 718 paint.setTextAlign(Align.CENTER); 719 final float hintX = keyWidth - params.mKeyHintLetterPadding 720 - getCharWidth(KEY_LABEL_REFERENCE_CHAR, paint) / 2; 721 final float hintY = keyHeight - params.mKeyPopupHintLetterPadding; 722 canvas.drawText(POPUP_HINT_CHAR, hintX, hintY, paint); 723 724 if (debugShowAlign) { 725 final Paint line = new Paint(); 726 drawHorizontalLine(canvas, (int)hintY, keyWidth, 0xc0808000, line); 727 drawVerticalLine(canvas, (int)hintX, keyHeight, 0xc0808000, line); 728 } 729 } 730 } 731 732 private static final Rect sTextBounds = new Rect(); 733 getCharGeometryCacheKey(char reference, Paint paint)734 private static int getCharGeometryCacheKey(char reference, Paint paint) { 735 final int labelSize = (int)paint.getTextSize(); 736 final Typeface face = paint.getTypeface(); 737 final int codePointOffset = reference << 15; 738 if (face == Typeface.DEFAULT) { 739 return codePointOffset + labelSize; 740 } else if (face == Typeface.DEFAULT_BOLD) { 741 return codePointOffset + labelSize + 0x1000; 742 } else if (face == Typeface.MONOSPACE) { 743 return codePointOffset + labelSize + 0x2000; 744 } else { 745 return codePointOffset + labelSize; 746 } 747 } 748 getCharHeight(char[] character, Paint paint)749 private static float getCharHeight(char[] character, Paint paint) { 750 final Integer key = getCharGeometryCacheKey(character[0], paint); 751 final Float cachedValue = sTextHeightCache.get(key); 752 if (cachedValue != null) 753 return cachedValue; 754 755 paint.getTextBounds(character, 0, 1, sTextBounds); 756 final float height = sTextBounds.height(); 757 sTextHeightCache.put(key, height); 758 return height; 759 } 760 getCharWidth(char[] character, Paint paint)761 private static float getCharWidth(char[] character, Paint paint) { 762 final Integer key = getCharGeometryCacheKey(character[0], paint); 763 final Float cachedValue = sTextWidthCache.get(key); 764 if (cachedValue != null) 765 return cachedValue; 766 767 paint.getTextBounds(character, 0, 1, sTextBounds); 768 final float width = sTextBounds.width(); 769 sTextWidthCache.put(key, width); 770 return width; 771 } 772 getLabelWidth(CharSequence label, Paint paint)773 private static float getLabelWidth(CharSequence label, Paint paint) { 774 paint.getTextBounds(label.toString(), 0, label.length(), sTextBounds); 775 return sTextBounds.width(); 776 } 777 getDefaultLabelWidth(CharSequence label, Paint paint)778 public float getDefaultLabelWidth(CharSequence label, Paint paint) { 779 paint.setTextSize(mKeyDrawParams.mKeyLabelSize); 780 paint.setTypeface(mKeyDrawParams.mKeyTextStyle); 781 return getLabelWidth(label, paint); 782 } 783 drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, int height)784 private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width, 785 int height) { 786 canvas.translate(x, y); 787 icon.setBounds(0, 0, width, height); 788 icon.draw(canvas); 789 canvas.translate(-x, -y); 790 } 791 drawHorizontalLine(Canvas canvas, float y, float w, int color, Paint paint)792 private static void drawHorizontalLine(Canvas canvas, float y, float w, int color, 793 Paint paint) { 794 paint.setStyle(Paint.Style.STROKE); 795 paint.setStrokeWidth(1.0f); 796 paint.setColor(color); 797 canvas.drawLine(0, y, w, y, paint); 798 } 799 drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint)800 private static void drawVerticalLine(Canvas canvas, float x, float h, int color, Paint paint) { 801 paint.setStyle(Paint.Style.STROKE); 802 paint.setStrokeWidth(1.0f); 803 paint.setColor(color); 804 canvas.drawLine(x, 0, x, h, paint); 805 } 806 drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, Paint paint)807 private static void drawRectangle(Canvas canvas, float x, float y, float w, float h, int color, 808 Paint paint) { 809 paint.setStyle(Paint.Style.STROKE); 810 paint.setStrokeWidth(1.0f); 811 paint.setColor(color); 812 canvas.translate(x, y); 813 canvas.drawRect(0, 0, w, h, paint); 814 canvas.translate(-x, -y); 815 } 816 cancelAllMessages()817 public void cancelAllMessages() { 818 mDrawingHandler.cancelAllMessages(); 819 } 820 821 // Called by {@link PointerTracker} constructor to create a TextView. 822 @Override inflateKeyPreviewText()823 public TextView inflateKeyPreviewText() { 824 final Context context = getContext(); 825 if (mKeyPreviewLayoutId != 0) { 826 return (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null); 827 } else { 828 return new TextView(context); 829 } 830 } 831 832 @Override showKeyPreview(int keyIndex, PointerTracker tracker)833 public void showKeyPreview(int keyIndex, PointerTracker tracker) { 834 if (mShowKeyPreviewPopup) { 835 mDrawingHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker); 836 } 837 } 838 839 @Override cancelShowKeyPreview(PointerTracker tracker)840 public void cancelShowKeyPreview(PointerTracker tracker) { 841 mDrawingHandler.cancelShowKeyPreview(tracker); 842 } 843 844 @Override dismissKeyPreview(PointerTracker tracker)845 public void dismissKeyPreview(PointerTracker tracker) { 846 mDrawingHandler.cancelShowKeyPreview(tracker); 847 mDrawingHandler.dismissKeyPreview(mDelayAfterPreview, tracker); 848 } 849 addKeyPreview(TextView keyPreview)850 private void addKeyPreview(TextView keyPreview) { 851 if (mPreviewPlacer == null) { 852 mPreviewPlacer = new RelativeLayout(getContext()); 853 final ViewGroup windowContentView = 854 (ViewGroup)getRootView().findViewById(android.R.id.content); 855 windowContentView.addView(mPreviewPlacer); 856 } 857 mPreviewPlacer.addView( 858 keyPreview, FrameLayoutCompatUtils.newLayoutParam(mPreviewPlacer, 0, 0)); 859 } 860 showKey(final int keyIndex, PointerTracker tracker)861 private void showKey(final int keyIndex, PointerTracker tracker) { 862 final TextView previewText = tracker.getKeyPreviewText(); 863 // If the key preview has no parent view yet, add it to the ViewGroup which can place 864 // key preview absolutely in SoftInputWindow. 865 if (previewText.getParent() == null) { 866 addKeyPreview(previewText); 867 } 868 869 mDrawingHandler.cancelDismissKeyPreview(tracker); 870 final Key key = tracker.getKey(keyIndex); 871 // If keyIndex is invalid or IME is already closed, we must not show key preview. 872 // Trying to show key preview while root window is closed causes 873 // WindowManager.BadTokenException. 874 if (key == null) 875 return; 876 877 final KeyPreviewDrawParams params = mKeyPreviewDrawParams; 878 final int keyDrawX = key.mX + key.mVisualInsetsLeft; 879 final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight; 880 // What we show as preview should match what we show on key top in onBufferDraw(). 881 if (key.mLabel != null) { 882 // TODO Should take care of temporaryShiftLabel here. 883 previewText.setCompoundDrawables(null, null, null, null); 884 if (key.mLabel.length() > 1) { 885 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize); 886 previewText.setTypeface(Typeface.DEFAULT_BOLD); 887 } else { 888 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mPreviewTextSize); 889 previewText.setTypeface(params.mKeyTextStyle); 890 } 891 previewText.setText(mKeyboard.adjustLabelCase(key.mLabel)); 892 } else { 893 final Drawable previewIcon = key.getPreviewIcon(); 894 previewText.setCompoundDrawables(null, null, null, 895 previewIcon != null ? previewIcon : key.getIcon()); 896 previewText.setText(null); 897 } 898 previewText.setBackgroundDrawable(params.mPreviewBackground); 899 900 previewText.measure( 901 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 902 final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth 903 + previewText.getPaddingLeft() + previewText.getPaddingRight()); 904 final int previewHeight = params.mPreviewHeight; 905 getLocationInWindow(params.mCoordinates); 906 int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + params.mCoordinates[0]; 907 final int previewY = key.mY - previewHeight 908 + params.mCoordinates[1] + params.mPreviewOffset; 909 if (previewX < 0 && params.mPreviewLeftBackground != null) { 910 previewText.setBackgroundDrawable(params.mPreviewLeftBackground); 911 previewX = 0; 912 } else if (previewX + previewWidth > getWidth() && params.mPreviewRightBackground != null) { 913 previewText.setBackgroundDrawable(params.mPreviewRightBackground); 914 previewX = getWidth() - previewWidth; 915 } 916 917 // Set the preview background state 918 previewText.getBackground().setState( 919 key.mMoreKeys != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); 920 previewText.setTextColor(params.mPreviewTextColor); 921 FrameLayoutCompatUtils.placeViewAt( 922 previewText, previewX, previewY, previewWidth, previewHeight); 923 previewText.setVisibility(VISIBLE); 924 } 925 926 /** 927 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient 928 * because the keyboard renders the keys to an off-screen buffer and an invalidate() only 929 * draws the cached buffer. 930 * @see #invalidateKey(Key) 931 */ invalidateAllKeys()932 public void invalidateAllKeys() { 933 mDirtyRect.union(0, 0, getWidth(), getHeight()); 934 mBufferNeedsUpdate = true; 935 invalidate(); 936 } 937 938 /** 939 * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only 940 * one key is changing it's content. Any changes that affect the position or size of the key 941 * may not be honored. 942 * @param key key in the attached {@link Keyboard}. 943 * @see #invalidateAllKeys 944 */ 945 @Override invalidateKey(Key key)946 public void invalidateKey(Key key) { 947 if (key == null) 948 return; 949 mInvalidatedKey = key; 950 final int x = key.mX + getPaddingLeft(); 951 final int y = key.mY + getPaddingTop(); 952 mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight); 953 mDirtyRect.union(mInvalidatedKeyRect); 954 mBufferNeedsUpdate = true; 955 invalidate(mInvalidatedKeyRect); 956 } 957 closing()958 public void closing() { 959 PointerTracker.dismissAllKeyPreviews(); 960 cancelAllMessages(); 961 962 mDirtyRect.union(0, 0, getWidth(), getHeight()); 963 requestLayout(); 964 } 965 966 @Override dismissMoreKeysPanel()967 public boolean dismissMoreKeysPanel() { 968 return false; 969 } 970 purgeKeyboardAndClosing()971 public void purgeKeyboardAndClosing() { 972 mKeyboard = null; 973 closing(); 974 } 975 976 @Override onDetachedFromWindow()977 protected void onDetachedFromWindow() { 978 super.onDetachedFromWindow(); 979 closing(); 980 if (mPreviewPlacer != null) { 981 mPreviewPlacer.removeAllViews(); 982 } 983 if (mBuffer != null) { 984 mBuffer.recycle(); 985 mBuffer = null; 986 } 987 } 988 } 989