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,但说实话,车厂一般不会直接用。为什么呢?因为车机麦克风阵列的降噪算法和手机不一样,车内风噪、胎噪、空调声,都会影响识别率。
我建议的做法是:输入法内部集成语音入口,但把识别逻辑交给车厂的语音服务。输入法只负责两件事:
- 提供一个语音按钮,点击后启动车厂自带的语音识别 Activity。
- 接收语音识别结果,通过
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 的定制。为什么这么排?因为 Service 定制是框架层的事,一次写好后面基本不动。而语音和键盘是用户天天打交道的,体验好坏直接影响口碑。
好了,本章的内容就到这里。车载输入法这块,说白了就是「安全第一,效率第二」。你设计的每一个按键、每一次语音交互,都要问自己一句:司机在 80km/h 的速度下,能安全地用吗?