预上屏是什么鬼?KikaInputMethod 输入预测功能深度解析
文章目录预上屏的本质预上屏执行流程核心预上屏代码Enter 键确认上屏光标操作全集私有命令通信sendPrivateCommand物理键盘处理onKeyDownInputClient 关键接口速查踩坑记录写在最后用搜狗或者系统键盘打字时打到一半会出现候选词框选中候选词之前的虚显效果就叫预上屏Preview Text。KikaInputMethod demo 里用了一套完整的预上屏实现还有文字选择、光标移动等进阶操作。预上屏的本质正常打字用户按一个键 → 字符立即出现在文本框里已上屏预上屏用户按了几个键 → 先显示正在候选的文字虚线下划线样式→ 用户选择候选词 → 候选内容正式上屏或者修改后上屏用大白话说预上屏就是先占位确认后再落地。预上屏执行流程核心预上屏代码KeyboardController.ets里的insertText方法publicinsertText(text:string):void{if(!this.mTextInputClient)return;try{// 第一步普通插入字符立即上屏this.mTextInputClient.insertTextSync(text);// 第二步获取光标位置letindexCursor:numberthis.mTextInputClient.getTextIndexAtCursorSync();// 第三步读取光标前 2 个字符letlength2;lettextPre:stringthis.mTextInputClient.getForwardSync(length);// 注意getForwardSync 不包含刚插入的字符需要手动加上textPretextPretext;if(textPrehel){// 打了 hel触发预上屏 hello world// range 的 end 要 1 把当前字符也包含进选区letrange:inputMethodEngine.Range{start:indexCursor-length,end:indexCursor1};// 设置预上屏把 range 区域的内容替换为 hello world 的虚显this.mTextInputClient.setPreviewText(hello world,range).then((){console.log(预上屏设置成功);});}elseif(this.intputText.lengthlength1){// 已经在预上屏过程中继续打字letindexSubStrStartthis.intputText.lastIndexOf(hel);if(indexSubStrStart0){letsubStrthis.intputText.substring(indexSubStrStart,indexCursor);// 如果当前已输入内容是 hello world 的前缀继续更新预上屏if(subStr!hello worldhello world.includes(subStr)){letrange:inputMethodEngine.Range{start:indexSubStrStart,end:indexCursor1};this.mTextInputClient.setPreviewText(hello world,range);}}}}catch(err){console.error(insertText error:${JSON.stringify(err)});}}Enter 键确认上屏publicsendKeyFunction():void{if(this.mTextInputClientthis.mEditorAttribute){// 根据编辑框的 enterKeyType 发送对应功能// enterKeyType: 0默认换行, 1搜索, 2发送, 3下一项, 4完成this.mTextInputClient.sendKeyFunction(this.mEditorAttribute.enterKeyType).then((){console.log(sendKeyFunction 成功);});// 结束预上屏状态让虚显的文字正式落地this.mTextInputClient.finishTextPreview().then((){console.log(finishTextPreview 成功);});}}光标操作全集InputHandler 里封装了完整的光标操作这些是输入法进阶功能的基础import{inputMethodEngine}fromkit.IMEKit;// 移动光标上/下/左/右publicmoveCursor(direction:inputMethodEngine.Direction):void{// Direction.CURSOR_UP/DOWN/LEFT/RIGHTthis.mTextInputClient?.moveCursor(direction);}// 光标移到开头publicasyncmoveCursorToBegin():Promisevoid{// selectByRange({start:0, end:0}) 等效于光标移到 0 位置awaitthis.mTextInputClient?.selectByRange({start:0,end:0});}// 光标移到末尾publicasyncmoveCursorToEnd():Promisevoid{// 用 1000 作为无穷大实际上文本没有 1000 字符时会停在末尾awaitthis.mTextInputClient?.selectByRange({start:1000,end:1000});}// 按方向选中文本Shift 方向键的效果publicselectByMovement(direction:inputMethodEngine.Direction):void{this.mTextInputClient?.selectByMovement({direction:direction});}// 从当前光标位置选到开头publicasyncselectToBegin():Promisevoid{letindexawaitthis.mTextInputClient!.getTextIndexAtCursor();if(index0){awaitthis.mTextInputClient?.selectByRange({start:0,end:index});}}// 从当前光标位置选到末尾publicasyncselectToEnd():Promisevoid{letindexawaitthis.mTextInputClient!.getTextIndexAtCursor();awaitthis.mTextInputClient?.selectByRange({start:index,end:1000});}私有命令通信sendPrivateCommand输入法和应用之间可以通过私有命令传递自定义数据// 输入法端发送私有命令给宿主应用publiconInputStart():void{letrecord:Recordstring,inputMethodEngine.CommandDataType{previewTextStyle:underline// 自定义字段};this.mTextInputClient?.sendPrivateCommand(record);}// 输入法端接收宿主应用发来的私有命令inputMethodAbility.on(privateCommand,(record:Recordstring,inputMethodEngine.CommandDataType){Object.keys(record).forEach((key:string){console.log(key:${key}, value:${record[key]});});});这个功能适合做输入法和应用之间的定制化协议比如应用告诉输入法我是密码框只显示数字键盘。物理键盘处理onKeyDown当外接了蓝牙键盘或设备本身有实体键盘时KeyboardDelegate的keyDown事件会触发publiconKeyDown(keyEvent:inputMethodEngine.KeyEvent):boolean{letkeyCodekeyEvent.keyCode;// 维护当前按下的键列表用于检测组合键this.keyCodes.push(keyCode);// 处理功能键letkeyValuegetHardKeyValue(keyCode,this.isShiftKeyHold());returnthis.inputHardKeyCode(keyValue,keyCode);}publicprocessFunctionKeys(keyValue:string):boolean{switch(keyValue){caseKEYCODE_DEL:this.inputHandle.deleteForward(1);returntrue;// 返回 true 消费系统不再处理caseKEYCODE_FORWARD_DEL:this.inputHandle.deleteBackward(1);returntrue;caseKEYCODE_DPAD_UP:this.inputHandle.moveCursor(inputMethodEngine.Direction.CURSOR_UP);returntrue;caseKEYCODE_DPAD_LEFT:this.inputHandle.moveCursor(inputMethodEngine.Direction.CURSOR_LEFT);returntrue;// ...default:returnfalse;// 返回 false 不消费交给系统}}InputClient 关键接口速查接口用途同步/异步insertTextSync(text)插入文字同步deleteForward(n)向前删 n 个字符异步deleteBackward(n)向后删 n 个字符异步getForwardSync(n)获取光标前 n 个字符同步getBackwardSync(n)获取光标后 n 个字符同步getTextIndexAtCursorSync()获取光标位置同步moveCursor(direction)移动光标异步selectByRange(range)选中范围异步setPreviewText(text, range)设置预上屏异步finishTextPreview()结束预上屏异步sendKeyFunction(enterKeyType)发送 Enter 功能异步getEditorAttribute()获取编辑框属性异步sendPrivateCommand(record)发送私有命令异步sendExtendAction(action)发送扩展操作剪切/复制/全选等异步踩坑记录坑1getForwardSync 不包含刚插入的字符insertTextSync后立即调getForwardSync读出来的内容不包含刚插入的那个字符。原因是预上屏状态下字符在虚显区域还没正式提交。代码里需要手动textPre textPre text补上。坑2selectByRange({start:1000, end:1000}) 是移动光标到末尾的常见技巧没有直接的光标到末尾接口用selectByRange传一个大于文本长度的值即可系统会自动截断到实际末尾。坑3finishTextPreview 必须在 sendKeyFunction 之后调如果反过来预上屏内容会被清空Enter 键的功能也不会正常触发。坑4私有命令的 value 类型只支持 boolean/number/stringCommandDataType是boolean | number | string的联合类型不能传对象或数组。复杂数据需要序列化成字符串再传。写在最后预上屏功能让输入法的候选词体验更流畅避免了候选词替换后光标位置错误的问题。虽然 demo 里只是个hel → hello world的演示但背后的接口和机制是完整的照着这个框架实现真实的拼音/联想输入完全可行。