blob: 45d52e825cf7ca99e1a01522abb1f865eea252ec [file] [log] [blame]
/*
* 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<Key> 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<Key> keys) {
mKeys = new KeyHolder[keys.size()];
Iterator<Key> itt = keys.iterator();
for (int i = 0; i < mKeys.length && itt.hasNext(); i++) {
Key k = itt.next();
mKeys[i] = new KeyHolder(k);
}
}
}