Android TVAndroid TVAndroid开发

【 Android 】Android TV 系统开发 —— 输入

2017-07-26  本文已影响431人  Tyhoo_Wu

今天就来聊一聊 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 的大环境(即原生代码)

  1. 添加 LatinIME.apk 到 Device 里。
    路径:
device\定制厂商的名字\  

找到关联 APK 的 .mk 文件,在里面添加:

PRODUCT_PACKAGES += LatinIME
  1. 修改 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 。

  1. 自定义按键被选中时的边框
    路径:
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 。

  1. 修改 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() 里面,
我分别对按键 上下左右、删除、确定、键盘类型间的切换做了处理。

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;
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;
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;
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;
case KeyEvent.KEYCODE_BACK:
    if (keyEvent.getRepeatCount() == 0 && mainKeyboardView != null) {
        if (mainKeyboardView.isShown()) {
            hideWindow();
        }
    }
    break;
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);

即,返回父类。

解决办法:

@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

开源的世界,需要大家的共同智慧!

上一篇下一篇

猜你喜欢

热点阅读