美文网首页Android TVAndroid TVAndroid开发
【 Android 】Android TV 系统开发 —— 输入

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

作者: Tyhoo_Wu | 来源:发表于2017-07-26 00:50 被阅读431次

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

    • 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

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

    相关文章

      网友评论

      • 倾城AllureLo_fe76:好像上下键有点问题啊
      • 鸩羽千夜92:博主 你的github 上 有这代码,能不能丢一个github 连接
      • 96fc63c5907a:曝光骗子宋文举: 简书昵称: wenju_song电话 13522410381 https://github.com/shengxinjing/programmer-job-blacklist/issues/136
      • 雅默之星:这焦点移动问题很严重啊,KEYCODE_DPAD_UP 中的for循环需要改一下,同时记忆一下上一个焦点:
        } else {
        if(mLastKeyIndexDown >= 0){//如果有记录,恢复到按【下键】前的焦点
        mainKeyboardView.setLastKeyIndex(mLastKeyIndexDown);
        mLastKeyIndexUp = mLastKeyIndex;//记录按【上键】前的焦点
        mLastKeyIndexDown = -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 = nearestKeyIndices.get(i);
        int nearIndex = getKeyIndex(nearKey);

        if (mLastKeyIndex > nearIndex) {
        Key lastKey = mKeys.get(mLastKeyIndex);
        if ((nearKey.getX() >= lastKey.getX())
        &&(nearKey.getX() <= (lastKey.getX() + lastKey.getWidth()))) {

        mainKeyboardView.setLastKeyIndex(nearIndex);
        mLastKeyIndexUp = mLastKeyIndex;//记录按【上键】前的焦点
        break;
        }
        }
        }
        }
        }
        按左右键要清一下mLastKeyIndexUp,mLastKeyIndexDown
        鸩羽千夜92:层主的法子可行,+1
      • trayliu_小马过河:KEYCODE_DPAD_UP 中的for循环应该是i >= 0 开始的吧
      • trayliu_小马过河:你们再用哪家方案?
      • 75b7383c3ce4:你在逗我吗?TV开发是从5.0之后开始的?
        某米,某视。都是从4.X 开始的。
        Tyhoo_Wu:@越来越好啊啊啊 我指的是安卓原生的,请打开 Android Studio 创建工程,选择 TV 项,看看最低要求是哪个版本,然后再过来叫唤。这种东西就好比 Android O 在手机上引入的 PIP,国内厂商也是早在7.0左右就是实现了。我要说的是原生开发,原生开发,原生开发!谢谢

      本文标题:【 Android 】Android TV 系统开发 —— 输入

      本文链接:https://www.haomeiwen.com/subject/qlbikxtx.html