1 /* 2 * Copyright (C) 2019 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.leanback; 18 19 import android.content.Context; 20 21 import java.util.ArrayList; 22 23 import android.content.res.Resources; 24 import android.content.res.TypedArray; 25 import android.content.res.XmlResourceParser; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.Paint; 29 import android.graphics.Paint.Align; 30 import android.graphics.Rect; 31 import android.graphics.Typeface; 32 import android.inputmethodservice.Keyboard; 33 import android.inputmethodservice.Keyboard.Key; 34 import android.inputmethodservice.Keyboard.Row; 35 import android.media.AudioManager; 36 import android.provider.Settings; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.view.accessibility.AccessibilityManager; 43 import android.widget.FrameLayout; 44 import android.widget.ImageView; 45 46 import java.util.HashMap; 47 import java.util.Iterator; 48 import java.util.List; 49 import java.util.Map; 50 51 public class LeanbackKeyboardView extends FrameLayout { 52 53 private static final String TAG = "LbKbView"; 54 private static final boolean DEBUG = false; 55 56 private static final int NOT_A_KEY = -1; 57 58 public static final int SHIFT_OFF = 0; 59 public static final int SHIFT_ON = 1; 60 public static final int SHIFT_LOCKED = 2; 61 private int mShiftState; 62 63 private final float mFocusedScale; 64 private final float mClickedScale; 65 private final int mClickAnimDur; 66 private final int mUnfocusStartDelay; 67 private final int mInactiveMiniKbAlpha; 68 69 private Keyboard mKeyboard; 70 private KeyHolder[] mKeys; 71 private ImageView[] mKeyImageViews; 72 73 private int mFocusIndex; 74 private boolean mFocusClicked; 75 private View mCurrentFocusView; 76 private boolean mMiniKeyboardOnScreen; 77 78 /** 79 * Special keycodes 80 */ 81 public static final int ASCII_SPACE = 32; 82 public static final int ASCII_PERIOD = 46; 83 public static final int KEYCODE_SHIFT = -1; 84 public static final int KEYCODE_SYM_TOGGLE = -2; 85 public static final int KEYCODE_LEFT = -3; 86 public static final int KEYCODE_RIGHT = -4; 87 public static final int KEYCODE_DELETE = -5; 88 public static final int KEYCODE_CAPS_LOCK = -6; 89 public static final int KEYCODE_VOICE = -7; 90 public static final int KEYCODE_DISMISS_MINI_KEYBOARD = -8; 91 92 private int mBaseMiniKbIndex = -1; 93 94 private Paint mPaint; 95 private Rect mPadding; 96 private int mModeChangeTextSize; 97 private int mKeyTextSize; 98 private int mKeyTextColor; 99 private int mRowCount; 100 private int mColCount; 101 102 private class KeyHolder { 103 public boolean isInMiniKb = false; 104 public boolean isInvertible = false; 105 public Key key; 106 KeyHolder(Key key)107 public KeyHolder(Key key) { 108 this.key = key; 109 } 110 } 111 LeanbackKeyboardView(Context context, AttributeSet attrs)112 public LeanbackKeyboardView(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 115 final Resources res = context.getResources(); 116 TypedArray a = context.getTheme() 117 .obtainStyledAttributes(attrs, R.styleable.LeanbackKeyboardView, 0, 0); 118 mRowCount = a.getInteger(R.styleable.LeanbackKeyboardView_rowCount, -1); 119 mColCount = a.getInteger(R.styleable.LeanbackKeyboardView_columnCount, -1); 120 121 mKeyTextSize = (int) res.getDimension(R.dimen.key_font_size); 122 123 mPaint = new Paint(); 124 mPaint.setAntiAlias(true); 125 mPaint.setTextSize(mKeyTextSize); 126 mPaint.setTextAlign(Align.CENTER); 127 mPaint.setAlpha(255); 128 129 mPadding = new Rect(0, 0, 0, 0); 130 131 mModeChangeTextSize = (int) res.getDimension(R.dimen.function_key_mode_change_font_size); 132 133 mKeyTextColor = res.getColor(R.color.key_text_default); 134 135 mFocusIndex = -1; 136 137 mShiftState = SHIFT_OFF; 138 139 mFocusedScale = res.getFraction(R.fraction.focused_scale, 1, 1); 140 mClickedScale = res.getFraction(R.fraction.clicked_scale, 1, 1); 141 mClickAnimDur = res.getInteger(R.integer.clicked_anim_duration); 142 mUnfocusStartDelay = res.getInteger(R.integer.unfocused_anim_delay); 143 144 mInactiveMiniKbAlpha = res.getInteger(R.integer.inactive_mini_kb_alpha); 145 } 146 147 /** 148 * Get the total rows of the keyboard 149 */ getRowCount()150 public int getRowCount() { 151 return mRowCount; 152 } 153 154 /** 155 * Get the total columns of the keyboard 156 */ getColCount()157 public int getColCount() { 158 return mColCount; 159 } 160 161 /** 162 * Get the key at the specified index 163 * 164 * @param index 165 * @return null if the keyboardView has not been assigned a keyboard 166 */ getKey(int index)167 public Key getKey(int index) { 168 if (mKeys == null || mKeys.length == 0 || index < 0 || index > mKeys.length) { 169 return null; 170 } 171 return mKeys[index].key; 172 } 173 174 /** 175 * Get the current focused key 176 */ getFocusedKey()177 public Key getFocusedKey() { 178 return mFocusIndex == -1 ? null : mKeys[mFocusIndex].key; 179 } 180 181 /** 182 * Get the keyboard that's attached to the keyboardView 183 */ getKeyboard()184 public Keyboard getKeyboard() { 185 return mKeyboard; 186 } 187 188 /** 189 * Get the key that's the nearest to the given position 190 * 191 * @param x position in pixels 192 * @param y position in pixels 193 */ getNearestIndex(float x, float y)194 public int getNearestIndex(float x, float y) { 195 if (mKeys == null || mKeys.length == 0) { 196 return 0; 197 } 198 x -= getPaddingLeft(); 199 y -= getPaddingTop(); 200 float height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); 201 float width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); 202 int rows = getRowCount(); 203 int cols = getColCount(); 204 int row = (int) (y / height * rows); 205 if (row < 0) { 206 row = 0; 207 } else if (row >= rows) { 208 row = rows - 1; 209 } 210 int col = (int) (x / width * cols); 211 if (col < 0) { 212 col = 0; 213 } else if (col >= cols) { 214 col = cols - 1; 215 } 216 int index = mColCount * row + col; 217 218 // at space key (space key is 7 keys wide) 219 if (index > 46 && index < 53) { 220 index = 46; 221 } 222 223 // beyond space, remove 6 extra slots for space 224 if (index >= 53) { 225 index -= 6; 226 } 227 228 if (index < 0) { 229 index = 0; 230 } else if (index >= mKeys.length) { 231 index = mKeys.length - 1; 232 } 233 234 return index; 235 } 236 237 /** 238 * Attaches a keyboard to this view. The keyboard can be switched at any 239 * time and the view will re-layout itself to accommodate the keyboard. 240 * 241 * @see Keyboard 242 * @see #getKeyboard() 243 * @param keyboard the keyboard to display in this view 244 */ setKeyboard(Keyboard keyboard)245 public void setKeyboard(Keyboard keyboard) { 246 // Remove any pending messages 247 removeMessages(); 248 mKeyboard = keyboard; 249 setKeys(mKeyboard.getKeys()); 250 251 // reset shift state 252 int shiftState = mShiftState; 253 mShiftState = -1; 254 setShiftState(shiftState); 255 256 requestLayout(); 257 invalidateAllKeys(); 258 // computeProximityThreshold(keyboard); // TODO 259 } 260 createKeyImageView(int keyIndex)261 private ImageView createKeyImageView(int keyIndex) { 262 263 final Rect padding = mPadding; 264 final int kbdPaddingLeft = getPaddingLeft(); 265 final int kbdPaddingTop = getPaddingTop(); 266 final KeyHolder keyHolder = mKeys[keyIndex]; 267 final Key key = keyHolder.key; 268 269 // Switch the character to uppercase if shift is pressed 270 adjustCase(keyHolder); 271 String label = key.label == null ? null : key.label.toString(); 272 if (Log.isLoggable(TAG, Log.VERBOSE)) { 273 Log.d(TAG, "LABEL: " + key.label + "->" + label); 274 } 275 276 Bitmap bitmap = Bitmap.createBitmap(key.width, key.height, Bitmap.Config.ARGB_8888); 277 Canvas canvas = new Canvas(bitmap); 278 final Paint paint = mPaint; 279 paint.setColor(mKeyTextColor); 280 281 canvas.drawARGB(0, 0, 0, 0); 282 283 if (key.icon != null) { 284 if (key.codes[0] == Keyboard.KEYCODE_SHIFT) { 285 switch (mShiftState) { 286 case SHIFT_OFF: 287 key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_off); 288 break; 289 case SHIFT_ON: 290 key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_on); 291 break; 292 case SHIFT_LOCKED: 293 key.icon = getContext().getResources() 294 .getDrawable(R.drawable.ic_ime_shift_lock_on); 295 break; 296 } 297 } 298 final int drawableX = (key.width - padding.left - padding.right 299 - key.icon.getIntrinsicWidth()) / 2 + padding.left; 300 final int drawableY = (key.height - padding.top - padding.bottom 301 - key.icon.getIntrinsicHeight()) / 2 + padding.top; 302 canvas.translate(drawableX, drawableY); 303 key.icon.setBounds(0, 0, 304 key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); 305 key.icon.draw(canvas); 306 canvas.translate(-drawableX, -drawableY); 307 } else if (label != null) { 308 // For characters, use large font. For labels like "Done", use 309 // small font. 310 if (label.length() > 1) { 311 paint.setTextSize(mModeChangeTextSize); 312 paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); 313 } else { 314 paint.setTextSize(mKeyTextSize); 315 paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); 316 } 317 // Draw the text 318 canvas.drawText(label, 319 (key.width - padding.left - padding.right) / 2 320 + padding.left, 321 (key.height - padding.top - padding.bottom) / 2 322 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, 323 paint); 324 // Turn off drop shadow 325 paint.setShadowLayer(0, 0, 0, 0); 326 } 327 328 ImageView view = new ImageView(getContext()); 329 view.setImageBitmap(bitmap); 330 view.setContentDescription(label); 331 addView(view, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, 332 LayoutParams.WRAP_CONTENT)); 333 334 view.setX(key.x + kbdPaddingLeft); 335 view.setY(key.y + kbdPaddingTop); 336 view.setImageAlpha(mMiniKeyboardOnScreen && !keyHolder.isInMiniKb ? 337 mInactiveMiniKbAlpha : 255); 338 view.setVisibility(View.VISIBLE); 339 340 return view; 341 } 342 createKeyImageViews(KeyHolder[] keys)343 private void createKeyImageViews(KeyHolder[] keys) { 344 int totalKeys = keys.length; 345 if (mKeyImageViews != null) { 346 for (ImageView view : mKeyImageViews) { 347 this.removeView(view); 348 } 349 mKeyImageViews = null; 350 } 351 352 for (int keyIndex = 0; keyIndex < totalKeys; keyIndex++) { 353 if (mKeyImageViews == null) { 354 mKeyImageViews = new ImageView[totalKeys]; 355 } else if (mKeyImageViews[keyIndex] != null) { 356 removeView(mKeyImageViews[keyIndex]); 357 } 358 mKeyImageViews[keyIndex] = createKeyImageView(keyIndex); 359 } 360 } 361 removeMessages()362 private void removeMessages() { 363 // TODO create mHandler and remove all messages here 364 } 365 366 /** 367 * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is 368 * not sufficient because the keyboard renders the keys to an off-screen 369 * buffer and an invalidate() only draws the cached buffer. 370 * 371 * @see #invalidateKey(int) 372 */ invalidateAllKeys()373 public void invalidateAllKeys() { 374 createKeyImageViews(mKeys); 375 } 376 invalidateKey(int keyIndex)377 public void invalidateKey(int keyIndex) { 378 if (mKeys == null) 379 return; 380 if (keyIndex < 0 || keyIndex >= mKeys.length) { 381 return; 382 } 383 if (mKeyImageViews[keyIndex] != null) { 384 removeView(mKeyImageViews[keyIndex]); 385 } 386 mKeyImageViews[keyIndex] = createKeyImageView(keyIndex); 387 } 388 389 @Override onDraw(Canvas canvas)390 public void onDraw(Canvas canvas) { 391 super.onDraw(canvas); 392 } 393 adjustCase(KeyHolder keyHolder)394 private CharSequence adjustCase(KeyHolder keyHolder) { 395 CharSequence label = keyHolder.key.label; 396 397 if (label != null && label.length() < 3) { 398 // if we're adjusting the case of a basic letter in the mini keyboard, 399 // we want the opposite case 400 boolean invert = keyHolder.isInMiniKb && keyHolder.isInvertible; 401 if (mKeyboard.isShifted() ^ invert) { 402 label = label.toString().toUpperCase(); 403 } else { 404 label = label.toString().toLowerCase(); 405 } 406 407 keyHolder.key.label = label; 408 } 409 410 return label; 411 } 412 setShiftState(int state)413 public void setShiftState(int state) { 414 if (mShiftState == state) { 415 return; 416 } 417 switch (state) { 418 case SHIFT_OFF: 419 mKeyboard.setShifted(false); 420 break; 421 case SHIFT_ON: 422 case SHIFT_LOCKED: 423 mKeyboard.setShifted(true); 424 break; 425 } 426 mShiftState = state; 427 invalidateAllKeys(); 428 } 429 getShiftState()430 public int getShiftState() { 431 return mShiftState; 432 } 433 isShifted()434 public boolean isShifted() { 435 return mShiftState == SHIFT_ON || mShiftState == SHIFT_LOCKED; 436 } 437 setFocus(int index, boolean clicked)438 public void setFocus(int index, boolean clicked) { 439 setFocus(index, clicked, true); 440 } 441 setFocus(int index, boolean clicked, boolean showFocusScale)442 public void setFocus(int index, boolean clicked, boolean showFocusScale) { 443 if (mKeyImageViews == null || mKeyImageViews.length == 0) { 444 return; 445 } 446 if (index < 0 || index >= mKeyImageViews.length) { 447 index = NOT_A_KEY; 448 } 449 450 if (index != mFocusIndex || clicked != mFocusClicked) { 451 if (index != mFocusIndex) { 452 if (mFocusIndex != NOT_A_KEY) { 453 LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[mFocusIndex], false); 454 } 455 if (index != NOT_A_KEY) { 456 LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[index], true); 457 } 458 } 459 460 if (mCurrentFocusView != null) { 461 mCurrentFocusView.animate().scaleX(1f).scaleY(1f) 462 .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) 463 .setStartDelay(mUnfocusStartDelay); 464 mCurrentFocusView.animate().setDuration(mClickAnimDur) 465 .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) 466 .setStartDelay(mUnfocusStartDelay); 467 } 468 if (index != NOT_A_KEY) { 469 float scale = clicked ? mClickedScale : (showFocusScale ? mFocusedScale : 1.0f); 470 mCurrentFocusView = mKeyImageViews[index]; 471 mCurrentFocusView.animate().scaleX(scale).scaleY(scale) 472 .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) 473 .setDuration(mClickAnimDur).start(); 474 } 475 mFocusIndex = index; 476 mFocusClicked = clicked; 477 478 // if focusing on a non-mini kb key, dismiss minikb 479 if (NOT_A_KEY != index && !mKeys[index].isInMiniKb) { 480 dismissMiniKeyboard(); 481 } 482 } 483 } 484 isMiniKeyboardOnScreen()485 public boolean isMiniKeyboardOnScreen() { 486 return mMiniKeyboardOnScreen; 487 } 488 onKeyLongPress()489 public void onKeyLongPress() { 490 int popupResId = mKeys[mFocusIndex].key.popupResId; 491 if (popupResId != 0) { 492 dismissMiniKeyboard(); 493 mMiniKeyboardOnScreen = true; 494 Keyboard miniKeyboard = new Keyboard(getContext(), popupResId); 495 List<Key> accentKeys = miniKeyboard.getKeys(); 496 int totalAccentKeys = accentKeys.size(); 497 int baseIndex = mFocusIndex; 498 int currentRow = mFocusIndex / mColCount; 499 int nextRow = (mFocusIndex + totalAccentKeys) / mColCount; 500 // if all accent keys don't fit in a row when aligned with the popup 501 // key, align the accent keys to the right boundary of that row 502 if (currentRow != nextRow) { 503 baseIndex = nextRow * mColCount - totalAccentKeys; 504 } 505 mBaseMiniKbIndex = baseIndex; 506 for (int i = 0; i < totalAccentKeys; i++) { 507 Key accentKey = accentKeys.get(i); 508 // inherit the key position and edge flags. this way the xml files for the each 509 // miniKb don't have to take into account the configuration of the keyboard 510 // they're being inserted into. 511 accentKey.x = mKeys[baseIndex + i].key.x; 512 accentKey.y = mKeys[baseIndex + i].key.y; 513 accentKey.edgeFlags = mKeys[baseIndex + i].key.edgeFlags; 514 mKeys[baseIndex + i].key = accentKey; 515 mKeys[baseIndex + i].isInMiniKb = true; 516 mKeys[baseIndex + i].isInvertible = (i == 0); 517 } 518 519 invalidateAllKeys(); 520 } 521 } 522 getBaseMiniKbIndex()523 public int getBaseMiniKbIndex() { 524 return mBaseMiniKbIndex; 525 } 526 527 /** 528 * @return true if the minikeyboard was on-screen and is now dismissed, false otherwise. 529 */ dismissMiniKeyboard()530 public boolean dismissMiniKeyboard() { 531 if (mMiniKeyboardOnScreen) { 532 mMiniKeyboardOnScreen = false; 533 setKeys(mKeyboard.getKeys()); 534 invalidateAllKeys(); 535 return true; 536 } 537 538 return false; 539 } 540 setFocus(int row, int col, boolean clicked)541 public void setFocus(int row, int col, boolean clicked) { 542 setFocus(mColCount * row + col, clicked); 543 } 544 545 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)546 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 547 // For the kids, ya know? 548 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 549 // Round up a little 550 if (mKeyboard == null) { 551 setMeasuredDimension(getPaddingLeft() + getPaddingRight(), 552 getPaddingTop() + getPaddingBottom()); 553 } else { 554 int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); 555 if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { 556 width = MeasureSpec.getSize(widthMeasureSpec); 557 } 558 setMeasuredDimension(width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); 559 } 560 } 561 setKeys(List<Key> keys)562 private void setKeys(List<Key> keys) { 563 mKeys = new KeyHolder[keys.size()]; 564 Iterator<Key> itt = keys.iterator(); 565 for (int i = 0; i < mKeys.length && itt.hasNext(); i++) { 566 Key k = itt.next(); 567 mKeys[i] = new KeyHolder(k); 568 } 569 } 570 } 571