/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.inputmethod.leanback; import android.content.Context; import java.util.ArrayList; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Rect; import android.graphics.Typeface; import android.inputmethodservice.Keyboard; import android.inputmethodservice.Keyboard.Key; import android.inputmethodservice.Keyboard.Row; import android.media.AudioManager; import android.provider.Settings; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.ImageView; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; public class LeanbackKeyboardView extends FrameLayout { private static final String TAG = "LbKbView"; private static final boolean DEBUG = false; private static final int NOT_A_KEY = -1; public static final int SHIFT_OFF = 0; public static final int SHIFT_ON = 1; public static final int SHIFT_LOCKED = 2; private int mShiftState; private final float mFocusedScale; private final float mClickedScale; private final int mClickAnimDur; private final int mUnfocusStartDelay; private final int mInactiveMiniKbAlpha; private Keyboard mKeyboard; private KeyHolder[] mKeys; private ImageView[] mKeyImageViews; private int mFocusIndex; private boolean mFocusClicked; private View mCurrentFocusView; private boolean mMiniKeyboardOnScreen; /** * Special keycodes */ public static final int ASCII_SPACE = 32; public static final int ASCII_PERIOD = 46; public static final int KEYCODE_SHIFT = -1; public static final int KEYCODE_SYM_TOGGLE = -2; public static final int KEYCODE_LEFT = -3; public static final int KEYCODE_RIGHT = -4; public static final int KEYCODE_DELETE = -5; public static final int KEYCODE_CAPS_LOCK = -6; public static final int KEYCODE_VOICE = -7; public static final int KEYCODE_DISMISS_MINI_KEYBOARD = -8; private int mBaseMiniKbIndex = -1; private Paint mPaint; private Rect mPadding; private int mModeChangeTextSize; private int mKeyTextSize; private int mKeyTextColor; private int mRowCount; private int mColCount; private class KeyHolder { public boolean isInMiniKb = false; public boolean isInvertible = false; public Key key; public KeyHolder(Key key) { this.key = key; } } public LeanbackKeyboardView(Context context, AttributeSet attrs) { super(context, attrs); final Resources res = context.getResources(); TypedArray a = context.getTheme() .obtainStyledAttributes(attrs, R.styleable.LeanbackKeyboardView, 0, 0); mRowCount = a.getInteger(R.styleable.LeanbackKeyboardView_rowCount, -1); mColCount = a.getInteger(R.styleable.LeanbackKeyboardView_columnCount, -1); mKeyTextSize = (int) res.getDimension(R.dimen.key_font_size); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setTextSize(mKeyTextSize); mPaint.setTextAlign(Align.CENTER); mPaint.setAlpha(255); mPadding = new Rect(0, 0, 0, 0); mModeChangeTextSize = (int) res.getDimension(R.dimen.function_key_mode_change_font_size); mKeyTextColor = res.getColor(R.color.key_text_default); mFocusIndex = -1; mShiftState = SHIFT_OFF; mFocusedScale = res.getFraction(R.fraction.focused_scale, 1, 1); mClickedScale = res.getFraction(R.fraction.clicked_scale, 1, 1); mClickAnimDur = res.getInteger(R.integer.clicked_anim_duration); mUnfocusStartDelay = res.getInteger(R.integer.unfocused_anim_delay); mInactiveMiniKbAlpha = res.getInteger(R.integer.inactive_mini_kb_alpha); } /** * Get the total rows of the keyboard */ public int getRowCount() { return mRowCount; } /** * Get the total columns of the keyboard */ public int getColCount() { return mColCount; } /** * Get the key at the specified index * * @param index * @return null if the keyboardView has not been assigned a keyboard */ public Key getKey(int index) { if (mKeys == null || mKeys.length == 0 || index < 0 || index > mKeys.length) { return null; } return mKeys[index].key; } /** * Get the current focused key */ public Key getFocusedKey() { return mFocusIndex == -1 ? null : mKeys[mFocusIndex].key; } /** * Get the keyboard that's attached to the keyboardView */ public Keyboard getKeyboard() { return mKeyboard; } /** * Get the key that's the nearest to the given position * * @param x position in pixels * @param y position in pixels */ public int getNearestIndex(float x, float y) { if (mKeys == null || mKeys.length == 0) { return 0; } x -= getPaddingLeft(); y -= getPaddingTop(); float height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); float width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); int rows = getRowCount(); int cols = getColCount(); int row = (int) (y / height * rows); if (row < 0) { row = 0; } else if (row >= rows) { row = rows - 1; } int col = (int) (x / width * cols); if (col < 0) { col = 0; } else if (col >= cols) { col = cols - 1; } int index = mColCount * row + col; // at space key (space key is 7 keys wide) if (index > 46 && index < 53) { index = 46; } // beyond space, remove 6 extra slots for space if (index >= 53) { index -= 6; } if (index < 0) { index = 0; } else if (index >= mKeys.length) { index = mKeys.length - 1; } return index; } /** * Attaches a keyboard to this view. The keyboard can be switched at any * time and the view will re-layout itself to accommodate the keyboard. * * @see Keyboard * @see #getKeyboard() * @param keyboard the keyboard to display in this view */ public void setKeyboard(Keyboard keyboard) { // Remove any pending messages removeMessages(); mKeyboard = keyboard; setKeys(mKeyboard.getKeys()); // reset shift state int shiftState = mShiftState; mShiftState = -1; setShiftState(shiftState); requestLayout(); invalidateAllKeys(); // computeProximityThreshold(keyboard); // TODO } private ImageView createKeyImageView(int keyIndex) { final Rect padding = mPadding; final int kbdPaddingLeft = getPaddingLeft(); final int kbdPaddingTop = getPaddingTop(); final KeyHolder keyHolder = mKeys[keyIndex]; final Key key = keyHolder.key; // Switch the character to uppercase if shift is pressed adjustCase(keyHolder); String label = key.label == null ? null : key.label.toString(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.d(TAG, "LABEL: " + key.label + "->" + label); } Bitmap bitmap = Bitmap.createBitmap(key.width, key.height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); final Paint paint = mPaint; paint.setColor(mKeyTextColor); canvas.drawARGB(0, 0, 0, 0); if (key.icon != null) { if (key.codes[0] == Keyboard.KEYCODE_SHIFT) { switch (mShiftState) { case SHIFT_OFF: key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_off); break; case SHIFT_ON: key.icon = getContext().getResources().getDrawable(R.drawable.ic_ime_shift_on); break; case SHIFT_LOCKED: key.icon = getContext().getResources() .getDrawable(R.drawable.ic_ime_shift_lock_on); break; } } final int drawableX = (key.width - padding.left - padding.right - key.icon.getIntrinsicWidth()) / 2 + padding.left; final int drawableY = (key.height - padding.top - padding.bottom - key.icon.getIntrinsicHeight()) / 2 + padding.top; canvas.translate(drawableX, drawableY); key.icon.setBounds(0, 0, key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight()); key.icon.draw(canvas); canvas.translate(-drawableX, -drawableY); } else if (label != null) { // For characters, use large font. For labels like "Done", use // small font. if (label.length() > 1) { paint.setTextSize(mModeChangeTextSize); paint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL)); } else { paint.setTextSize(mKeyTextSize); paint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL)); } // Draw the text canvas.drawText(label, (key.width - padding.left - padding.right) / 2 + padding.left, (key.height - padding.top - padding.bottom) / 2 + (paint.getTextSize() - paint.descent()) / 2 + padding.top, paint); // Turn off drop shadow paint.setShadowLayer(0, 0, 0, 0); } ImageView view = new ImageView(getContext()); view.setImageBitmap(bitmap); view.setContentDescription(label); addView(view, new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); view.setX(key.x + kbdPaddingLeft); view.setY(key.y + kbdPaddingTop); view.setImageAlpha(mMiniKeyboardOnScreen && !keyHolder.isInMiniKb ? mInactiveMiniKbAlpha : 255); view.setVisibility(View.VISIBLE); return view; } private void createKeyImageViews(KeyHolder[] keys) { int totalKeys = keys.length; if (mKeyImageViews != null) { for (ImageView view : mKeyImageViews) { this.removeView(view); } mKeyImageViews = null; } for (int keyIndex = 0; keyIndex < totalKeys; keyIndex++) { if (mKeyImageViews == null) { mKeyImageViews = new ImageView[totalKeys]; } else if (mKeyImageViews[keyIndex] != null) { removeView(mKeyImageViews[keyIndex]); } mKeyImageViews[keyIndex] = createKeyImageView(keyIndex); } } private void removeMessages() { // TODO create mHandler and remove all messages here } /** * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is * not sufficient because the keyboard renders the keys to an off-screen * buffer and an invalidate() only draws the cached buffer. * * @see #invalidateKey(int) */ public void invalidateAllKeys() { createKeyImageViews(mKeys); } public void invalidateKey(int keyIndex) { if (mKeys == null) return; if (keyIndex < 0 || keyIndex >= mKeys.length) { return; } if (mKeyImageViews[keyIndex] != null) { removeView(mKeyImageViews[keyIndex]); } mKeyImageViews[keyIndex] = createKeyImageView(keyIndex); } @Override public void onDraw(Canvas canvas) { super.onDraw(canvas); } private CharSequence adjustCase(KeyHolder keyHolder) { CharSequence label = keyHolder.key.label; if (label != null && label.length() < 3) { // if we're adjusting the case of a basic letter in the mini keyboard, // we want the opposite case boolean invert = keyHolder.isInMiniKb && keyHolder.isInvertible; if (mKeyboard.isShifted() ^ invert) { label = label.toString().toUpperCase(); } else { label = label.toString().toLowerCase(); } keyHolder.key.label = label; } return label; } public void setShiftState(int state) { if (mShiftState == state) { return; } switch (state) { case SHIFT_OFF: mKeyboard.setShifted(false); break; case SHIFT_ON: case SHIFT_LOCKED: mKeyboard.setShifted(true); break; } mShiftState = state; invalidateAllKeys(); } public int getShiftState() { return mShiftState; } public boolean isShifted() { return mShiftState == SHIFT_ON || mShiftState == SHIFT_LOCKED; } public void setFocus(int index, boolean clicked) { setFocus(index, clicked, true); } public void setFocus(int index, boolean clicked, boolean showFocusScale) { if (mKeyImageViews == null || mKeyImageViews.length == 0) { return; } if (index < 0 || index >= mKeyImageViews.length) { index = NOT_A_KEY; } if (index != mFocusIndex || clicked != mFocusClicked) { if (index != mFocusIndex) { if (mFocusIndex != NOT_A_KEY) { LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[mFocusIndex], false); } if (index != NOT_A_KEY) { LeanbackUtils.sendAccessibilityEvent(mKeyImageViews[index], true); } } if (mCurrentFocusView != null) { mCurrentFocusView.animate().scaleX(1f).scaleY(1f) .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) .setStartDelay(mUnfocusStartDelay); mCurrentFocusView.animate().setDuration(mClickAnimDur) .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) .setStartDelay(mUnfocusStartDelay); } if (index != NOT_A_KEY) { float scale = clicked ? mClickedScale : (showFocusScale ? mFocusedScale : 1.0f); mCurrentFocusView = mKeyImageViews[index]; mCurrentFocusView.animate().scaleX(scale).scaleY(scale) .setInterpolator(LeanbackKeyboardContainer.sMovementInterpolator) .setDuration(mClickAnimDur).start(); } mFocusIndex = index; mFocusClicked = clicked; // if focusing on a non-mini kb key, dismiss minikb if (NOT_A_KEY != index && !mKeys[index].isInMiniKb) { dismissMiniKeyboard(); } } } public boolean isMiniKeyboardOnScreen() { return mMiniKeyboardOnScreen; } public void onKeyLongPress() { int popupResId = mKeys[mFocusIndex].key.popupResId; if (popupResId != 0) { dismissMiniKeyboard(); mMiniKeyboardOnScreen = true; Keyboard miniKeyboard = new Keyboard(getContext(), popupResId); List accentKeys = miniKeyboard.getKeys(); int totalAccentKeys = accentKeys.size(); int baseIndex = mFocusIndex; int currentRow = mFocusIndex / mColCount; int nextRow = (mFocusIndex + totalAccentKeys) / mColCount; // if all accent keys don't fit in a row when aligned with the popup // key, align the accent keys to the right boundary of that row if (currentRow != nextRow) { baseIndex = nextRow * mColCount - totalAccentKeys; } mBaseMiniKbIndex = baseIndex; for (int i = 0; i < totalAccentKeys; i++) { Key accentKey = accentKeys.get(i); // inherit the key position and edge flags. this way the xml files for the each // miniKb don't have to take into account the configuration of the keyboard // they're being inserted into. accentKey.x = mKeys[baseIndex + i].key.x; accentKey.y = mKeys[baseIndex + i].key.y; accentKey.edgeFlags = mKeys[baseIndex + i].key.edgeFlags; mKeys[baseIndex + i].key = accentKey; mKeys[baseIndex + i].isInMiniKb = true; mKeys[baseIndex + i].isInvertible = (i == 0); } invalidateAllKeys(); } } public int getBaseMiniKbIndex() { return mBaseMiniKbIndex; } /** * @return true if the minikeyboard was on-screen and is now dismissed, false otherwise. */ public boolean dismissMiniKeyboard() { if (mMiniKeyboardOnScreen) { mMiniKeyboardOnScreen = false; setKeys(mKeyboard.getKeys()); invalidateAllKeys(); return true; } return false; } public void setFocus(int row, int col, boolean clicked) { setFocus(mColCount * row + col, clicked); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // For the kids, ya know? super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Round up a little if (mKeyboard == null) { setMeasuredDimension(getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom()); } else { int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight(); if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) { width = MeasureSpec.getSize(widthMeasureSpec); } setMeasuredDimension(width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom()); } } private void setKeys(List keys) { mKeys = new KeyHolder[keys.size()]; Iterator itt = keys.iterator(); for (int i = 0; i < mKeys.length && itt.hasNext(); i++) { Key k = itt.next(); mKeys[i] = new KeyHolder(k); } } }