文章目录先搞清楚输入法的本质整体架构图第一层InputMethodExtensionAbility 注册module.json5 配置ServiceExtAbility.ets — 输入法入口第二层KeyboardController 初始化键盘面板第三层InputHandler 文本操作封装第四层监听注册第五层键盘 UI 组件使用 InputHandler踩坑记录写在最后自定义输入法是 HarmonyOS 里相对复杂的能力因为它不是一个普通页面而是一个系统级的扩展能力ExtensionAbility。KikaInputMethod这个 demo 把整个架构做得很完整值得深入学习。先搞清楚输入法的本质普通 App 的 UIAbility 就是有界面的进程而输入法本质上是一个InputMethodExtensionAbility它没有独立的 Launch 入口用户无法直接打开它只有当某个应用有输入框聚焦时系统才会召唤它它显示的键盘是一个Panel面板由系统管理位置用大白话说输入法就是一个被系统召唤的 UI 插件它自己没有 main 函数。整体架构图第一层InputMethodExtensionAbility 注册module.json5 配置{ module: { extensionAbilities: [ { name: InputMethodExtAbility, srcEntry: ./ets/InputMethodExtensionAbility/InputMethodService.ets, type: inputMethod, // 必须是 inputMethod 类型 exported: true, label: $string:app_name, // 指向键盘 UI 页面 metadata: [ { name: ohos.extension.input_method, resource: $profile:input_method_config } ] } ] } }ServiceExtAbility.ets — 输入法入口import{InputMethodExtensionAbility}fromkit.IMEKit;import{keyboardController}from./model/KeyboardController;import{Want}fromkit.AbilityKit;exportdefaultclassServiceExtAbilityextendsInputMethodExtensionAbility{onCreate(want:Want):void{// 输入法被系统激活第一次调起时执行一次keyboardController.onCreate(this.context);}onDestroy():void{// 输入法被卸载或系统要求销毁keyboardController.onDestroy();}}这个文件很薄核心逻辑都在KeyboardController里职责分离很干净。第二层KeyboardController 初始化键盘面板import{inputMethodEngine,InputMethodExtensionContext}fromkit.IMEKit;import{display}fromkit.ArkUI;import{StyleConfiguration}from../../common/StyleConfiguration;constinputMethodAbilityinputMethodEngine.getInputMethodAbility();classKeyboardController{privatepanel:inputMethodEngine.Panel|undefined;privatemContext:InputMethodExtensionContext|undefined;publiconCreate(context:InputMethodExtensionContext):void{this.mContextcontext;this.initWindow();// 创建键盘面板this.registerListener();// 注册所有监听}privateinitWindow():void{// 1. 获取屏幕信息计算键盘高度letdisdisplay.getDefaultDisplaySync();letdWidthdis.width;letdHeightdis.height;// 2. 根据设备类型、横竖屏计算键盘高度比例letkeyHeightRateKEYBOARD_HEIGHT_RATE_DEFAULT;// 默认 0.43letisLandscapedWidthdHeight;if(dWidth1344dHeight2772){// 标准手机竖屏键盘占 38% 高度keyHeightRateKEYBOARD_HEIGHT_RATE_PHONE;// 0.38}elseif(dWidth2772dHeight1344){// 手机横屏键盘占 50% 高度keyHeightRateKEYBOARD_HEIGHT_RATE_PHONE_LAND;// 0.5}// ... 其他设备适配letkeyHeightdHeight*keyHeightRate;// 3. 计算 StyleConfiguration键盘 UI 样式letinputStyleStyleConfiguration.getInputStyle(isLandscape,isRkDevice,deviceInfo.deviceType);AppStorage.setOrCreate(inputStyle,inputStyle);// 全局共享给键盘 UI// 4. 创建键盘面板letpanelInfo:inputMethodEngine.PanelInfo{type:inputMethodEngine.PanelType.SOFT_KEYBOARD,// 软键盘类型flag:inputMethodEngine.PanelFlag.FLG_FIXED// 固定在底部};inputMethodAbility.createPanel(this.mContext,panelInfo).then((panel:inputMethodEngine.Panel){this.panelpanel;// 5. 设置面板尺寸panel.resize(dWidth,keyHeight).then((){// 6. 加载键盘 UI 页面panel.setUiContent(InputMethodExtensionAbility/pages/Index);});});}}关键理解Panel 是系统提供的浮动窗口容器setUiContent把你的键盘 UI 页面加载进去。这个 Panel 的位置由系统控制固定在屏幕底部你只能控制高度。第三层InputHandler 文本操作封装InputHandler是单例封装了所有对文本框的操作import{inputMethodEngine}fromkit.IMEKit;exportclassInputHandler{privatemTextInputClient:inputMethodEngine.InputClient|undefined;privatemKbController:inputMethodEngine.KeyboardController|undefined;// 单例模式存在 AppStorage 里publicstaticgetInstance():InputHandler{letinstanceAppStorage.getInputHandler(inputHandler);if(instanceundefined){instancenewInputHandler();AppStorage.setOrCreate(inputHandler,instance);}returninstance;}// 系统调起输入法时触发拿到 KeyboardController 和 InputClientpubliconInputStart(kbController:inputMethodEngine.KeyboardController,textInputClient:inputMethodEngine.InputClient):void{this.mKbControllerkbController;this.mTextInputClienttextInputClient;// 获取编辑框属性Enter 键类型、输入模式等textInputClient.getEditorAttribute().then((attr){AppStorage.setOrCreate(enterKeyType,attr.enterKeyType);AppStorage.setOrCreate(inputPattern,attr.inputPattern);});}// 插入文字同步接口更快publicinsertText(text:string):void{if(this.mTextInputClient){this.mTextInputClient.insertTextSync(text);}}// 向前删除 n 个字符publicdeleteForward(length:number):void{if(this.mTextInputClient){this.mTextInputClient.deleteForward(length);}}// 向后删除 n 个字符publicdeleteBackward(length:number):void{if(this.mTextInputClient){this.mTextInputClient.deleteBackward(length);}}// 移动光标publicmoveCursor(direction:inputMethodEngine.Direction):void{if(this.mTextInputClient){this.mTextInputClient.moveCursor(direction);}}// 隐藏键盘publichideKeyboardSelf():void{if(this.mKbController){this.mKbController.hide();}}// 发送 Enter 键功能搜索/换行/完成等publicsendKeyFunction():void{if(this.mTextInputClientthis.mEditorAttribute){// 根据 enterKeyType 发送对应功能this.mTextInputClient.sendKeyFunction(this.mEditorAttribute.enterKeyType);// 结束预上屏this.mTextInputClient.finishTextPreview();}}}第四层监听注册privateregisterListener():void{// 1. 屏幕旋转时重新计算键盘尺寸display.on(change,(){this.resizePanel();});// 2. 有输入框聚焦时触发inputMethodAbility.on(inputStart,(kbController,textInputClient){this.inputHandle.onInputStart(kbController,textInputClient);});// 3. 输入法切换子类型比如中文/英文切换inputMethodAbility.on(setSubtype,(subtype){if(subtype.idInputMethodExtAbility){AppStorage.setOrCreate(subtypeChange,0);}});// 4. 输入框失焦/应用关闭时触发inputMethodAbility.on(inputStop,(){this.onDestroy();this.mContext?.destroy();});// 5. 物理键盘按键事件外接键盘或实体键盘设备this.mKeyboardDelegateinputMethodEngine.getKeyboardDelegate();this.mKeyboardDelegate.on(keyDown,(keyEvent){returnthis.onKeyDown(keyEvent);// 返回 true 表示消费此事件});this.mKeyboardDelegate.on(keyUp,(keyEvent){returnthis.onKeyUp(keyEvent);});// 6. 光标位置变化this.mKeyboardDelegate.on(cursorContextChange,(x,y,height){this.inputHandle.setCursorInfo({x,y,height});});}第五层键盘 UI 组件使用 InputHandler键盘上每个键点击时都通过InputHandler.getInstance()调用文本操作// KeyItem.ets — 普通字母键Componentexportstruct KeyItem{keyValue:string;build(){Stack(){Text(this.keyValue)}.onClick((){InputHandler.getInstance().insertText(this.keyValue);})}}// DeleteItem.ets — 删除键Componentexportstruct DeleteItem{build(){Stack(){Image($r(app.media.back))}.onClick((){InputHandler.getInstance().deleteForward(1);})}}// ReturnItem.ets — 确认/换行键Componentexportstruct ReturnItem{build(){Stack(){Image($r(app.media.return))}.onClick((){InputHandler.getInstance().sendKeyFunction();})}}// SpaceItem.ets — 空格键Componentexportstruct SpaceItem{spaceWith:Resource|undefinedundefined;// 从 AppStorage 读取当前样式StorageLink(inputStyle)inputStyle:KeyStyleStyleConfiguration.getSavedInputStyle();build(){Stack(){Text(space).fontSize(this.inputStyle.symbol_fontSize)// 样式自适应}.onClick((){InputHandler.getInstance().insertText( );})}}踩坑记录坑1Panel 只能在 initWindow 里创建一次不要在inputStart回调里创建 Panel每次有输入框聚焦都会触发inputStart重复创建会报错。Panel 应该在onCreate→initWindow时创建一次之后复用。坑2InputHandler 必须用单例键盘 UI 组件和 KeyboardController 在不同的调用链里它们需要共享同一个InputClient引用。用AppStorage存单例是这个 demo 的正确做法。坑3屏幕旋转必须重新 resize监听display.on(change)然后调this.panel.resize(newWidth, newHeight)是必须的否则键盘在旋转后会错位或大小不对。坑4物理键盘 onKeyDown 返回值决定是否消费返回true表示输入法消费了这个按键系统不再处理返回false表示交给系统。删除键KEYCODE_DEL要返回true否则系统也会处理一次删两个字符。写在最后开发自定义输入法比普通 App 复杂不少主要是多了一层系统框架的理解Panel 是系统给的容器InputClient 是系统给的文本操作接口你不能直接操作被编辑的文本框。但 KikaInputMethod 这个 demo 的架构分层很清晰照着写一遍就能明白整个机制。