当前位置: 首页 > news >正文

从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解

文章目录

      • 先搞清楚输入法的本质
      • 整体架构图
      • 第一层:InputMethodExtensionAbility 注册
        • module.json5 配置
        • ServiceExtAbility.ets — 输入法入口
      • 第二层:KeyboardController 初始化键盘面板
      • 第三层:InputHandler 文本操作封装
      • 第四层:监听注册
      • 第五层:键盘 UI 组件使用 InputHandler
      • 踩坑记录
      • 写在最后

自定义输入法是 HarmonyOS 里相对复杂的能力,因为它不是一个普通页面,而是一个系统级的扩展能力(ExtensionAbility)。KikaInputMethod这个 demo 把整个架构做得很完整,值得深入学习。

先搞清楚输入法的本质

普通 App 的 UIAbility 就是"有界面的进程",而输入法本质上是一个InputMethodExtensionAbility,它:

  1. 没有独立的 Launch 入口(用户无法直接打开它)
  2. 只有当某个应用有输入框聚焦时,系统才会召唤它
  3. 它显示的键盘是一个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}from'@kit.IMEKit';import{keyboardController}from'./model/KeyboardController';import{Want}from'@kit.AbilityKit';exportdefaultclassServiceExtAbilityextendsInputMethodExtensionAbility{onCreate(want:Want):void{// 输入法被系统激活(第一次调起时执行一次)keyboardController.onCreate(this.context);}onDestroy():void{// 输入法被卸载或系统要求销毁keyboardController.onDestroy();}}

这个文件很薄,核心逻辑都在KeyboardController里,职责分离很干净。

第二层:KeyboardController 初始化键盘面板

import{inputMethodEngine,InputMethodExtensionContext}from'@kit.IMEKit';import{display}from'@kit.ArkUI';import{StyleConfiguration}from'../../common/StyleConfiguration';constinputMethodAbility=inputMethodEngine.getInputMethodAbility();classKeyboardController{privatepanel:inputMethodEngine.Panel|undefined;privatemContext:InputMethodExtensionContext|undefined;publiconCreate(context:InputMethodExtensionContext):void{this.mContext=context;this.initWindow();// 创建键盘面板this.registerListener();// 注册所有监听}privateinitWindow():void{// 1. 获取屏幕信息,计算键盘高度letdis=display.getDefaultDisplaySync();letdWidth=dis.width;letdHeight=dis.height;// 2. 根据设备类型、横竖屏计算键盘高度比例letkeyHeightRate=KEYBOARD_HEIGHT_RATE_DEFAULT;// 默认 0.43letisLandscape=dWidth>dHeight;if(dWidth===1344&&dHeight===2772){// 标准手机竖屏:键盘占 38% 高度keyHeightRate=KEYBOARD_HEIGHT_RATE_PHONE;// 0.38}elseif(dWidth===2772&&dHeight===1344){// 手机横屏:键盘占 50% 高度keyHeightRate=KEYBOARD_HEIGHT_RATE_PHONE_LAND;// 0.5}// ... 其他设备适配letkeyHeight=dHeight*keyHeightRate;// 3. 计算 StyleConfiguration(键盘 UI 样式)letinputStyle=StyleConfiguration.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.panel=panel;// 5. 设置面板尺寸panel.resize(dWidth,keyHeight).then(()=>{// 6. 加载键盘 UI 页面panel.setUiContent('InputMethodExtensionAbility/pages/Index');});});}}

关键理解:Panel 是系统提供的"浮动窗口容器",setUiContent把你的键盘 UI 页面加载进去。这个 Panel 的位置由系统控制(固定在屏幕底部),你只能控制高度。


第三层:InputHandler 文本操作封装

InputHandler是单例,封装了所有对文本框的操作:

import{inputMethodEngine}from'@kit.IMEKit';exportclassInputHandler{privatemTextInputClient:inputMethodEngine.InputClient|undefined;privatemKbController:inputMethodEngine.KeyboardController|undefined;// 单例模式,存在 AppStorage 里publicstaticgetInstance():InputHandler{letinstance=AppStorage.get<InputHandler>('inputHandler');if(instance===undefined){instance=newInputHandler();AppStorage.setOrCreate('inputHandler',instance);}returninstance;}// 系统调起输入法时触发,拿到 KeyboardController 和 InputClientpubliconInputStart(kbController:inputMethodEngine.KeyboardController,textInputClient:inputMethodEngine.InputClient):void{this.mKbController=kbController;this.mTextInputClient=textInputClient;// 获取编辑框属性(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.mTextInputClient&&this.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.id==='InputMethodExtAbility'){AppStorage.setOrCreate('subtypeChange',0);}});// 4. 输入框失焦/应用关闭时触发inputMethodAbility.on('inputStop',()=>{this.onDestroy();this.mContext?.destroy();});// 5. 物理键盘按键事件(外接键盘或实体键盘设备)this.mKeyboardDelegate=inputMethodEngine.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|undefined=undefined;// 从 AppStorage 读取当前样式@StorageLink('inputStyle')inputStyle:KeyStyle=StyleConfiguration.getSavedInputStyle();build(){Stack(){Text('space').fontSize(this.inputStyle.symbol_fontSize)// 样式自适应}.onClick(()=>{InputHandler.getInstance().insertText(' ');})}}

踩坑记录

坑1:Panel 只能在 initWindow 里创建一次

不要在inputStart回调里创建 Panel,每次有输入框聚焦都会触发inputStart,重复创建会报错。Panel 应该在onCreateinitWindow时创建一次,之后复用。

坑2:InputHandler 必须用单例

键盘 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 的架构分层很清晰,照着写一遍就能明白整个机制。

http://www.jsqmd.com/news/855411/

相关文章:

  • AI 水印攻防战:OpenAI 引入 SynthID 认证,GitHub 同步出现去水印工具
  • 告别手动算长度!Allegro 17.4实战:用Relative Propagation Delay高效管理USB3.0差分对等长
  • 2026年天津驶入式货架厂家推荐与选型指南 - 品牌宣传支持者
  • 给RV1126 SDK‘打补丁’:如何在不污染源码的情况下,安全地添加和调试自己的rkmedia测试程序
  • 部署Nexus仓库总失败?可能是Ubuntu根目录空间不够!手把手教你排查并彻底解决磁盘占用问题
  • 2026年靠谱的钢制货架/仓储货架实力工厂推荐 - 行业平台推荐
  • 别再只盯着准确率了!用sklearn的cross_val_score时,这5个scoring参数选对了模型效果翻倍
  • 正规的驱蚊系统生产商口碑
  • 告别Mac与Windows传文件烦恼:一招教你将APFS格式的移动硬盘永久改成ExFAT通用格式
  • 2026西北区域车牌识别系统技术解析与选型参考:甘肃电动卷帘门、甘肃直杆道闸、甘肃自动卷帘门、甘肃车牌识别系统选择指南 - 优质品牌商家
  • 笔试训练48天:小乐乐改数字
  • 当流程图XML“损坏”时:手把手教你用Activiti API解析与修复BPMN文件
  • XUnity.AutoTranslator:打破游戏语言障碍的终极解决方案
  • 2026年市电路灯厂家地址盘点:甘肃ed路灯/甘肃哪有买太阳能路灯/甘肃太阳能路灯价格/甘肃太阳能路灯加工厂/甘肃太阳能路灯厂家电话/选择指南 - 优质品牌商家
  • TensorRT在Win11上装完怎么用?一个简单Python脚本验证你的安装是否真的成功
  • 2026年兰州卫生纸批发商家排行及采购务实参考:兰州哪个地方卫生纸批发便宜/兰州哪有批发卫生纸的/兰州城关卫生纸批发/选择指南 - 优质品牌商家
  • 瑞芯微RK3572正式发布,中阶AIoT八核处理器,性能功耗双突破
  • 如何免费解锁百度网盘macOS版SVIP功能:终极完整指南
  • 实验室御用MedPeer科研绘图工具实测
  • 别再只用按键了!用STM32F103的ADC读取电位器,给你的无感无刷电机做个“油门”
  • 终极Windows驱动清理指南:3分钟快速释放C盘隐藏空间
  • 2026年商业空间隔断厂家排行及选型实用指南:甘肃卫生间隔断/甘肃双玻百叶隔断/甘肃成品隔断/甘肃活动隔断/甘肃玻璃隔墙/选择指南 - 优质品牌商家
  • 从Python安装到数据分析:新手避坑指南与实战项目路线图
  • 统信UOS/麒麟KYLINOS批量部署神器:用dpkg -i和yes命令搞定交互式deb包静默安装
  • 用Field II和MATLAB搞定超声波声场仿真:从理论推导到代码实战(附源码)
  • DHT11温湿度数据不准?可能是时序问题!用51单片机(STC12)和逻辑分析仪调试避坑指南
  • 【DeepSeek API接入实战指南】:20年AI架构师亲授5大避坑要点与3分钟快速调通秘籍
  • 红日靶场实战复盘:我是如何利用phpMyAdmin日志写入拿到WebShell的
  • 避坑指南:VMware安装RockyLinux后网络不通、SSH连不上的常见问题排查与修复
  • STM32串口1被占用了怎么办?巧用ISP模式,让蓝牙HC-05同时搞定下载和通信