25、车载输入法:InputMethodService定制、车载键盘设计、语音输入替代、多语言支持

各位同学,今天我们来聊聊车载输入法。说实话,这个模块在手机端已经被玩得很透了,但到了车上,一切都不一样了。

我最早接触车载输入法是在一个量产项目上。当时产品经理说:「就按手机输入法做,加个语音就行。」结果呢?测试同事一上车就骂娘——按键太小,开车时根本点不准。嗯,从那以后我明白了,车载输入法不是手机输入法的子集,它是一个全新的设计领域。

25.1 InputMethodService 定制:从零搭建车载输入法

Android 的输入法框架核心就是 InputMethodService。说白了,它就是系统与输入法之间的桥梁。你写的键盘界面、候选词逻辑、按键处理,全都要挂在这个 Service 上。

我个人的习惯是,先写一个基础骨架,再往里面填业务逻辑。来看一个最简单的车载输入法 Service:

public class CarInputMethodService extends InputMethodService {

    private CarKeyboardView keyboardView;
    private InputMethodManager imm;

    @Override
    public View onCreateInputView() {
        // 返回你的键盘布局
        keyboardView = (CarKeyboardView) getLayoutInflater()
                .inflate(R.layout.car_keyboard_view, null);
        keyboardView.setOnKeyboardActionListener(new CarKeyboardActionListener());
        return keyboardView;
    }

    @Override
    public void onStartInput(EditorInfo attribute, boolean restarting) {
        super.onStartInput(attribute, restarting);
        // 根据输入类型切换键盘模式
        switch (attribute.inputType & InputType.TYPE_MASK_CLASS) {
            case InputType.TYPE_CLASS_NUMBER:
                keyboardView.setKeyboard(new NumberKeyboard(this));
                break;
            case InputType.TYPE_CLASS_TEXT:
                keyboardView.setKeyboard(new QwertyKeyboard(this));
                break;
        }
    }

    private class CarKeyboardActionListener implements KeyboardView.OnKeyboardActionListener {
        @Override
        public void onKey(int primaryCode, int[] keyCodes) {
            InputConnection ic = getCurrentInputConnection();
            if (ic == null) return;
            switch (primaryCode) {
                case Keyboard.KEYCODE_DELETE:
                    ic.deleteSurroundingText(1, 0);
                    break;
                case Keyboard.KEYCODE_DONE:
                    ic.performEditorAction(EditorInfo.IME_ACTION_DONE);
                    break;
                default:
                    char code = (char) primaryCode;
                    ic.commitText(String.valueOf(code), 1);
                    break;
            }
        }
    }
}

这里有个坑,我必须要说。车载屏幕的 EditorInfo 经常被车厂魔改。比如有些车机把 inputType 设成了 TYPE_NULL,你如果不做兜底处理,输入法直接不弹出来。我曾经因为这个 bug 排查了两天,最后发现是车厂自己的 Launcher 传了个空类型。

注意:onStartInput 中一定要对 attribute.inputType 做默认值处理。如果检测到 TYPE_NULL,建议回退到 TYPE_CLASS_TEXT

25.2 车载键盘设计:大按键、少误触、高对比

车载键盘的设计原则,说白了就三个字:大、少、高

  • :按键最小尺寸建议 48dp x 48dp,最好做到 56dp 以上。我见过某德系车厂的键盘,按键做到 64dp,开车时盲打都没问题。
  • :不要堆功能键。Shift、Caps Lock、符号切换这些,能合并就合并。车载场景下,用户最多输入地址、歌名、电话号码,不需要全键盘。
  • :对比度要高。车载屏幕在阳光下反光严重,按键文字和背景的对比度建议不低于 4.5:1。

我推荐一种布局方案:双排字母 + 数字行 + 功能键。字母按 QWERTY 排列,但只保留两排,把不常用的字母(比如 Z、X、Q)放到第二排的次要位置。数字单独一行,放在最上面,方便输入地址门牌号。

核心思路: 车载键盘不是给「打字」用的,是给「点选」用的。每个按键都要大到手指能轻松命中,哪怕车在颠簸。

另外,按键的触摸反馈一定要做。我建议用 HapticFeedbackConstants.KEYBOARD_TAP,配合一个短促的震动。用户按下去,手指能感觉到「咔哒」一下,心理上会踏实很多。

25.3 语音输入替代:当键盘不再是主角

在车上,语音输入才是真正的王道。你想想看,开车时低头看键盘,哪怕只有两秒钟,也足够出事故了。所以,车载输入法的第一优先级不是键盘,而是语音。

Android Automotive OS 原生集成了 SpeechRecognizer,但说实话,车厂一般不会直接用。为什么呢?因为车机麦克风阵列的降噪算法和手机不一样,车内风噪、胎噪、空调声,都会影响识别率。

我建议的做法是:输入法内部集成语音入口,但把识别逻辑交给车厂的语音服务。输入法只负责两件事:

  1. 提供一个语音按钮,点击后启动车厂自带的语音识别 Activity。
  2. 接收语音识别结果,通过 InputConnection.commitText() 填入输入框。

来看一个简单的集成示例:

public class VoiceInputHelper {

    private static final String ACTION_VOICE_SEARCH = "com.car.voice.START_RECOGNITION";

    public static void startVoiceInput(Context context, InputConnection ic) {
        Intent intent = new Intent(ACTION_VOICE_SEARCH);
        intent.putExtra("target_package", context.getPackageName());
        intent.putExtra("callback_action", "com.car.voice.RESULT");

        // 启动车厂语音服务
        context.startActivity(intent);

        // 注册广播接收识别结果
        BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context ctx, Intent intent) {
                String result = intent.getStringExtra("recognized_text");
                if (result != null && ic != null) {
                    ic.commitText(result, 1);
                }
                context.unregisterReceiver(this);
            }
        };
        context.registerReceiver(receiver, 
                new IntentFilter("com.car.voice.RESULT"));
    }
}
经验之谈: 语音识别结果返回后,不要直接覆盖输入框。我习惯加一个「语音候选词」的浮层,让用户确认后再填入。因为车载语音识别率再高,也有翻车的时候,比如「望京」识别成「王京」。

25.4 多语言支持:不只是切换键盘布局

多语言支持在车载场景下,比手机复杂得多。为什么?因为车载输入法要同时服务司机和乘客,而司机可能是本地人,乘客可能是外国人。你不能让用户每次都要去设置里切换语言。

我推荐的做法是:多语言混合输入 + 自动检测

具体来说,输入法内部维护一个「活跃语言列表」,用户可以在设置里勾选常用的语言(比如中文、英文、日文)。输入时,键盘布局不变,但候选词引擎会同时从多个语言模型里取结果。

来看一个候选词引擎的简化实现:

public class MultiLangCandidateEngine {

    private List<LanguageModel> activeModels;

    public MultiLangCandidateEngine(List<String> languages) {
        activeModels = new ArrayList<>();
        for (String lang : languages) {
            activeModels.add(LanguageModelFactory.create(lang));
        }
    }

    public List<String> getCandidates(String input) {
        List<String> results = new ArrayList<>();
        for (LanguageModel model : activeModels) {
            List<String> candidates = model.predict(input);
            results.addAll(candidates);
        }
        // 按置信度排序,取前5个
        Collections.sort(results, (a, b) -> 
                Float.compare(getConfidence(b), getConfidence(a)));
        return results.subList(0, Math.min(5, results.size()));
    }

    private float getConfidence(String word) {
        // 实际项目中这里会调用语言模型的置信度接口
        return 1.0f;
    }
}

这里有个细节:中文和英文的输入逻辑完全不同。中文需要拼音转汉字,英文需要自动补全和纠错。我建议把语言模型做成插件化的,每种语言一个独立的 LanguageModel 实现类,通过工厂方法创建。这样新增语言时,不需要改核心代码。

避坑指南: 我曾经在日文输入上踩过坑。日文有平假名、片假名、汉字三种输入模式,而且用户经常混着用。如果你只做了拼音和英文的模型,日文用户会直接骂娘。所以,多语言支持一定要覆盖目标市场的所有主流输入习惯。

25.5 知识体系总览

下面这张图,我把本章的核心知识点串了起来。你可以把它当作一个检查清单,看看自己有没有遗漏。

车载输入法 InputMethodService 车载键盘设计 语音输入替代 多语言支持 onCreateInputView onStartInput InputConnection 大按键 48dp+ 双排字母布局 触觉反馈 SpeechRecognizer 车厂语音服务 候选词确认 混合输入 语言模型插件化 自动检测

这张图里,四个分支是并列关系。但实际开发中,语音输入的优先级最高,其次是键盘设计,然后是多语言支持,最后才是InputMethodService 的定制。为什么这么排?因为 Service 定制是框架层的事,一次写好后面基本不动。而语音和键盘是用户天天打交道的,体验好坏直接影响口碑。

好了,本章的内容就到这里。车载输入法这块,说白了就是「安全第一,效率第二」。你设计的每一个按键、每一次语音交互,都要问自己一句:司机在 80km/h 的速度下,能安全地用吗?


公众号:蓝海资料掘金营,微信deep3321