文章目录
-
-
- 先搞清楚输入法的本质
- 整体架构图
- [第一层:InputMethodExtensionAbility 注册](#第一层:InputMethodExtensionAbility 注册)
-
- [module.json5 配置](#module.json5 配置)
- [ServiceExtAbility.ets --- 输入法入口](#ServiceExtAbility.ets — 输入法入口)
- [第二层:KeyboardController 初始化键盘面板](#第二层:KeyboardController 初始化键盘面板)
- [第三层:InputHandler 文本操作封装](#第三层:InputHandler 文本操作封装)
- 第四层:监听注册
- [第五层:键盘 UI 组件使用 InputHandler](#第五层:键盘 UI 组件使用 InputHandler)
- 踩坑记录
- 写在最后
-
自定义输入法是 HarmonyOS 里相对复杂的能力,因为它不是一个普通页面,而是一个系统级的扩展能力(ExtensionAbility)。KikaInputMethod 这个 demo 把整个架构做得很完整,值得深入学习。
先搞清楚输入法的本质
普通 App 的 UIAbility 就是"有界面的进程",而输入法本质上是一个 InputMethodExtensionAbility,它:
- 没有独立的 Launch 入口(用户无法直接打开它)
- 只有当某个应用有输入框聚焦时,系统才会召唤它
- 它显示的键盘是一个 Panel(面板),由系统管理位置
用大白话说:输入法就是一个"被系统召唤的 UI 插件",它自己没有 main 函数。
整体架构图

第一层:InputMethodExtensionAbility 注册
module.json5 配置
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 --- 输入法入口
typescript
import { InputMethodExtensionAbility } from '@kit.IMEKit';
import { keyboardController } from './model/KeyboardController';
import { Want } from '@kit.AbilityKit';
export default class ServiceExtAbility extends InputMethodExtensionAbility {
onCreate(want: Want): void {
// 输入法被系统激活(第一次调起时执行一次)
keyboardController.onCreate(this.context);
}
onDestroy(): void {
// 输入法被卸载或系统要求销毁
keyboardController.onDestroy();
}
}
这个文件很薄,核心逻辑都在 KeyboardController 里,职责分离很干净。
第二层:KeyboardController 初始化键盘面板
typescript
import { inputMethodEngine, InputMethodExtensionContext } from '@kit.IMEKit';
import { display } from '@kit.ArkUI';
import { StyleConfiguration } from '../../common/StyleConfiguration';
const inputMethodAbility = inputMethodEngine.getInputMethodAbility();
class KeyboardController {
private panel: inputMethodEngine.Panel | undefined;
private mContext: InputMethodExtensionContext | undefined;
public onCreate(context: InputMethodExtensionContext): void {
this.mContext = context;
this.initWindow(); // 创建键盘面板
this.registerListener(); // 注册所有监听
}
private initWindow(): void {
// 1. 获取屏幕信息,计算键盘高度
let dis = display.getDefaultDisplaySync();
let dWidth = dis.width;
let dHeight = dis.height;
// 2. 根据设备类型、横竖屏计算键盘高度比例
let keyHeightRate = KEYBOARD_HEIGHT_RATE_DEFAULT; // 默认 0.43
let isLandscape = dWidth > dHeight;
if (dWidth === 1344 && dHeight === 2772) {
// 标准手机竖屏:键盘占 38% 高度
keyHeightRate = KEYBOARD_HEIGHT_RATE_PHONE; // 0.38
} else if (dWidth === 2772 && dHeight === 1344) {
// 手机横屏:键盘占 50% 高度
keyHeightRate = KEYBOARD_HEIGHT_RATE_PHONE_LAND; // 0.5
}
// ... 其他设备适配
let keyHeight = dHeight * keyHeightRate;
// 3. 计算 StyleConfiguration(键盘 UI 样式)
let inputStyle = StyleConfiguration.getInputStyle(
isLandscape, isRkDevice, deviceInfo.deviceType
);
AppStorage.setOrCreate('inputStyle', inputStyle); // 全局共享给键盘 UI
// 4. 创建键盘面板
let panelInfo: 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 是单例,封装了所有对文本框的操作:
typescript
import { inputMethodEngine } from '@kit.IMEKit';
export class InputHandler {
private mTextInputClient: inputMethodEngine.InputClient | undefined;
private mKbController: inputMethodEngine.KeyboardController | undefined;
// 单例模式,存在 AppStorage 里
public static getInstance(): InputHandler {
let instance = AppStorage.get<InputHandler>('inputHandler');
if (instance === undefined) {
instance = new InputHandler();
AppStorage.setOrCreate('inputHandler', instance);
}
return instance;
}
// 系统调起输入法时触发,拿到 KeyboardController 和 InputClient
public onInputStart(
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);
});
}
// 插入文字(同步接口,更快)
public insertText(text: string): void {
if (this.mTextInputClient) {
this.mTextInputClient.insertTextSync(text);
}
}
// 向前删除 n 个字符
public deleteForward(length: number): void {
if (this.mTextInputClient) {
this.mTextInputClient.deleteForward(length);
}
}
// 向后删除 n 个字符
public deleteBackward(length: number): void {
if (this.mTextInputClient) {
this.mTextInputClient.deleteBackward(length);
}
}
// 移动光标
public moveCursor(direction: inputMethodEngine.Direction): void {
if (this.mTextInputClient) {
this.mTextInputClient.moveCursor(direction);
}
}
// 隐藏键盘
public hideKeyboardSelf(): void {
if (this.mKbController) {
this.mKbController.hide();
}
}
// 发送 Enter 键功能(搜索/换行/完成等)
public sendKeyFunction(): void {
if (this.mTextInputClient && this.mEditorAttribute) {
// 根据 enterKeyType 发送对应功能
this.mTextInputClient.sendKeyFunction(this.mEditorAttribute.enterKeyType);
// 结束预上屏
this.mTextInputClient.finishTextPreview();
}
}
}
第四层:监听注册
typescript
private registerListener(): 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) => {
return this.onKeyDown(keyEvent); // 返回 true 表示消费此事件
});
this.mKeyboardDelegate.on('keyUp', (keyEvent) => {
return this.onKeyUp(keyEvent);
});
// 6. 光标位置变化
this.mKeyboardDelegate.on('cursorContextChange', (x, y, height) => {
this.inputHandle.setCursorInfo({ x, y, height });
});
}
第五层:键盘 UI 组件使用 InputHandler
键盘上每个键点击时,都通过 InputHandler.getInstance() 调用文本操作:
typescript
// KeyItem.ets --- 普通字母键
@Component
export struct KeyItem {
keyValue: string = '';
build() {
Stack() {
Text(this.keyValue)
}
.onClick(() => {
InputHandler.getInstance().insertText(this.keyValue);
})
}
}
// DeleteItem.ets --- 删除键
@Component
export struct DeleteItem {
build() {
Stack() {
Image($r('app.media.back'))
}
.onClick(() => {
InputHandler.getInstance().deleteForward(1);
})
}
}
// ReturnItem.ets --- 确认/换行键
@Component
export struct ReturnItem {
build() {
Stack() {
Image($r('app.media.return'))
}
.onClick(() => {
InputHandler.getInstance().sendKeyFunction();
})
}
}
// SpaceItem.ets --- 空格键
@Component
export struct 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 应该在 onCreate → initWindow 时创建一次,之后复用。
坑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 的架构分层很清晰,照着写一遍就能明白整个机制。