【 Android 】Android TV 系统开发 —— 输入
今天就来聊一聊 Android TV LatinIME 这部分。
先来看效果图:
整体示例图.png
当我接触这部分的时候,我会先去网上搜搜有没有类似的实现,借鉴他人的示例,但是我发现示例有是有,但是都太老了,现在是 Android N 甚至是 Android O 的时代,网上示例还都停留在 4.X 左右的版本,其实我还有一个疑问?Android TV 是从 Android 5.0 开始的,那些网上的大神们,你们咋写出来 5.0 以下的 Android TV 代码的?
参考图片.PNG废话就说到这里,开始在进入正题!
因为是系统开发,所以我们做系统开发的会拿到 Android 的源码进行二次开发。输入法就以 LatinIME 为例做讲解。输入法这部分代码会不断完善,并同步更新到 GitHub ,为开源世界做出一封贡献。
示例代码以 Android 7.1.2 为 Base 。
Android N Logo.png
LatinIME 的源码地址(需要科学上网)
https://android.googlesource.com/platform/packages/inputmethods/LatinIME/
示例 GIF ,来确定我所讲解的就是你想要的。
动态流程图.gif
好的,开始我们的输入法之旅!
调用 LatinIME
做 TV 开发的都会拿到 Android 的大环境(即原生代码)
- 添加 LatinIME.apk 到 Device 里。
路径:
device\定制厂商的名字\
找到关联 APK 的 .mk 文件,在里面添加:
PRODUCT_PACKAGES += LatinIME
- 修改 framework 层代码,关联 LatinIME 。
设置 APK 路径:
frameworks\base\packages\SettingsProvider\res\values
找到 defaults.xml 文件,在里面添加:
<string name="config_default_input_method" translatable="false">com.android.inputmethod.latin/.LatinIME</string>
APK 路径设置好之后,就要在类里面去调用:
frameworks\base\packages\SettingsProvider\src\com\android\providers\settings
找到 DatabaseHelper.java 文件,在 loadSecureSettings() 方法里面添加:
loadStringSetting(stmt, Settings.Secure.ENABLED_INPUT_METHODS, R.string.config_default_input_method );
设置好之后,我们就可以把 APK 烧到板子里,效果图如下:
分步示例.png
输入法弹出来了,但是问题来了,使用遥控器获取不到软键盘焦点。
重写 LatinIME ,获取软键盘焦点
LatinIME 对应于手机软键盘,都是触屏不用考虑焦点。但是 TV 要使用 LatinIME 就一定要考虑焦点的问题。(排除现在的触屏电视)
思路: 重写 onKeyDown()
实现:上下左右,大小写切换,字母键盘+数字键盘+符号键盘 之间的切换,删除键,确定键。
既然是二次开发原生代码,所以该实现的功能已经在手机里实现,但是在 TV 上没有进行适配。我们要做的就是适配 TV 。
- 自定义按键被选中时的边框
路径:
LatinIME\java\src\com\android\inputmethod\keyboard
找到 MainKeyboardView.java ,在里面重写 onDraw() 。
private List<Key> mKeys = new ArrayList<>();
private int mLastKeyIndex = 0;
private Key mFocusedKey;
private Rect mRect;
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
mCurrentKeyboard = this.getKeyboard();
mKeys = mCurrentKeyboard.getSortedKeys();
Paint p = new Paint();
p.setColor(Color.CYAN);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(3.75F);
// 大写键盘比小写键盘多一个字符,所以为了防止切换的时候出现异常,在这里将数值设为0。
if (mLastKeyIndex >= mKeys.size()) {
Log.d(TAG, "onDraw: mLastKeyIndex = " + mLastKeyIndex + " mKeys.size() = " + mKeys.size());
mLastKeyIndex = 0;
}
mFocusedKey = mKeys.get(mLastKeyIndex);
mRect = new Rect(
mFocusedKey.getX(), mFocusedKey.getY() + 4,
mFocusedKey.getX() + mFocusedKey.getWidth(),
mFocusedKey.getY() + mFocusedKey.getHeight()
);
canvas.drawRect(mRect, p);
}
Get 、 Set 最后一次键盘位置的信息
public int getLastKeyIndex() {
return mLastKeyIndex;
}
public void setLastKeyIndex(int index) {
this.mLastKeyIndex = index;
}
注意:这里面调用的 Key 和 Keyboard 的导入路径是:
LatinIME\java\src\com\android\inputmethod\keyboard
而非直接导入系统的 API 。
- 修改 InputMethodService 的子类
路径:
LatinIME\java\src\com\android\inputmethod\latin
自定义方法,用来包裹按键:
private int mCurKeyboardKeyNum;
private Keyboard mCurrentKeyboard;
private List<Key> mKeys;
private int mLastKeyIndex = 0;
private void setFields() {
if (mKeyboardSwitcher.getMainKeyboardView() == null) {
Log.d(TAG, "setFields MainKeyboardView = null");
return;
}
mCurrentKeyboard = mKeyboardSwitcher.getMainKeyboardView().getKeyboard();
mKeys = mCurrentKeyboard.getSortedKeys();
mCurKeyboardKeyNum = mKeys.size();
mLastKeyIndex = mKeyboardSwitcher.getMainKeyboardView().getLastKeyIndex();
}
private int getKeyIndex(Key key) {
if (key == null || !mKeys.contains(key)) {
return -1;
}
int index = mKeys.indexOf(key);
Log.d(TAG, "getKeyIndex: index = " + index);
return index;
}
找到 LatinIME.java ,在里面重写 onKeyDown() 。
通过上面的动态图,我们可以清晰地看到,在 OnKeyDown() 里面,
我分别对按键 上下左右、删除、确定、键盘类型间的切换做了处理。
- KEYCODE_DPAD_UP
case KeyEvent.KEYCODE_DPAD_UP:
if (mainKeyboardView == null) {
} else {
if (!mainKeyboardView.isShown()) {
} else {
setFields();
if (mLastKeyIndex <= 0) {
mainKeyboardView.setLastKeyIndex(mCurKeyboardKeyNum - 1);
} else {
List<Key> nearestKeyIndices = mCurrentKeyboard.getNearestKeys(
mKeys.get(mLastKeyIndex).getX(),
mKeys.get(mLastKeyIndex).getY());
for (int i = nearestKeyIndices.size() - 1; i > 0; i--) {
Key nearKey = mKeys.get(i);
int nearIndex = getKeyIndex(nearKey);
if (mLastKeyIndex > nearIndex) {
Key nextNearKey = mKeys.get(nearIndex + 1);
Key lastKey = mKeys.get(mLastKeyIndex);
if (((lastKey.getX() >= nearKey.getX())
&& (lastKey.getX() < (nearKey.getX() + nearKey.getWidth()))
&& (((lastKey.getX() + lastKey.getWidth()) <= (nextNearKey.getX() + nextNearKey.getWidth()))
|| ((lastKey.getX() + lastKey.getWidth()) > nextNearKey.getX())))) {
mainKeyboardView.setLastKeyIndex(nearIndex);
break;
}
}
}
}
mainKeyboardView.invalidate();
return true;
}
}
break;
- KEYCODE_DPAD_DOWN
case KeyEvent.KEYCODE_DPAD_DOWN:
if (mainKeyboardView == null) {
} else {
if (!mainKeyboardView.isShown()) {
} else {
setFields();
if (mLastKeyIndex >= mCurKeyboardKeyNum - 1) {
mainKeyboardView.setLastKeyIndex(0);
} else {
List<Key> nearestKeyIndices = mCurrentKeyboard.getNearestKeys(
mKeys.get(mLastKeyIndex).getX(),
mKeys.get(mLastKeyIndex).getY());
for (Key nearKey : nearestKeyIndices) {
int nearIndex = getKeyIndex(nearKey);
if (mLastKeyIndex < nearIndex) {
Key lastKey = mKeys.get(mLastKeyIndex);
if (((lastKey.getX() >= nearKey.getX())
&& (lastKey.getX() < (nearKey.getX() + nearKey.getWidth())))
|| (((lastKey.getX() + lastKey.getWidth()) > nearKey.getX())
&& ((lastKey.getX() + lastKey.getWidth()) <= (nearKey.getX() + nearKey.getWidth())))) {
mainKeyboardView.setLastKeyIndex(nearIndex);
break;
}
}
}
}
mainKeyboardView.invalidate();
return true;
}
}
break;
- KEYCODE_DPAD_LEFT
case KeyEvent.KEYCODE_DPAD_LEFT:
if (mainKeyboardView == null) {
} else {
if (!mainKeyboardView.isShown()) {
} else {
setFields();
if (mLastKeyIndex <= 0) {
mainKeyboardView.setLastKeyIndex(mCurKeyboardKeyNum - 1);
} else {
mLastKeyIndex--;
mainKeyboardView.setLastKeyIndex(mLastKeyIndex);
}
mainKeyboardView.invalidate();
return true;
}
}
break;
- KEYCODE_DPAD_RIGHT
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (mainKeyboardView == null) {
} else {
if (!mainKeyboardView.isShown()) {
} else {
setFields();
if (mLastKeyIndex >= mCurKeyboardKeyNum - 1) {
mainKeyboardView.setLastKeyIndex(0);
} else {
mLastKeyIndex++;
mainKeyboardView.setLastKeyIndex(mLastKeyIndex);
}
mainKeyboardView.invalidate();
return true;
}
}
break;
- KEYCODE_BACK
case KeyEvent.KEYCODE_BACK:
if (keyEvent.getRepeatCount() == 0 && mainKeyboardView != null) {
if (mainKeyboardView.isShown()) {
hideWindow();
}
}
break;
- KEYCODE_ENTER
case KeyEvent.KEYCODE_ENTER:
Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
if (keyboard != null && mainKeyboardView != null) {
if (mainKeyboardView.isShown()) {
if (keyboard.mId.isAlphabetKeyboard() || keyboard.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS || keyboard.mId.mElementId == KeyboardId.ELEMENT_SYMBOLS_SHIFTED) {
setFields();
int curKeyCode = mKeys.get(mLastKeyIndex).getCode();
mKeyDownHandled = true;
switch (curKeyCode) {
case Constants.CODE_DELETE:
case 10:
int keyX = mKeys.get(mLastKeyIndex).getX();
int keyY = mKeys.get(mLastKeyIndex).getY();
final Event event = createSoftwareKeypressEvent(getCodePointForKeyboard(curKeyCode),
keyX, keyY, false);
onEvent(event);
return true;
case Constants.CODE_SWITCH_ALPHA_SYMBOL:
case Constants.CODE_SHIFT:
onPressKey(curKeyCode, 0, false);
onReleaseKey(curKeyCode, false);
return true;
default:
CharSequence charSequence = String.valueOf((char) curKeyCode);
getCurrentInputConnection().commitText(String.valueOf((char) curKeyCode), 1);
return true;
}
}
}
}
break;
当这些按键设置完毕,运行代码,发现在只有 EditText 控件的条件下输入法一切正常。
但是当切换到 WIFI 连接页面,WIFI 连接页面是有 CheckBox 的,所以焦点问题就随之而来。经过调查和研究,得出的一个当前切实可行的办法(日后可能会做修改)。
调查:当我们点击完 onKeyDown 之后,抬起按键会继续执行 onKeyUp ,进而会执行
return super.onKeyUp(keyCode, keyEvent);
即,返回父类。
解决办法:
- 将 onKeyDown 里面的处理挪到 onKeyUp 去做。
(我们日常生活一贯是点击按钮的时候就做出响应,即在 onKeyDown 时,所以方法一不太符合日常使用习惯) - 在 onKeyUp 的时候做一下判断,当有 onKeyDown 响应,就 return,进而就不会返回到父类。
@Override
public boolean onKeyUp(final int keyCode, final KeyEvent keyEvent) {
if (mKeyDownHandled) {
mKeyDownHandled = false;
return true;
}
...
return super.onKeyUp(keyCode, keyEvent);
}
显然第二种方法,要比第一种更客户化,如果有更好的方法,欢迎补充,开源的世界需要大家的共同智慧。
至此,Android TV 的输入法那部分就算告一段落,以上代码仅供参考,日后如有更好的方法和改修,我会及时更新。
此 TV 的系统应该是截止发稿前最新的安卓版本 —— 7.1.2
开源的世界,需要大家的共同智慧!