查看: 212|回复: 0

HarmonyOS 7.0 Input Kit 键盘拦截与避让实战

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
在金融转账等企业级移动应用中,软键盘交互常常是影响留存和转化的关键环节。一个页面可能同时包含收款人姓名、银行卡号、转账金额和交易密码:姓名用系统输入法,金额需要带格式化校验和快捷金额的自定义数字键盘,密码则需要与系统级密码箱打通的安全键盘。早期做法中,多键盘切换容易出现页面闪烁、焦点丢失、输入框被遮挡;自定义键盘还会遇到系统软键盘“不请自来”,开发者只能用延迟失焦规避,容易引发焦点抢占。第三方输入法场景下,密码输入若缺少底层隔离,还会带来泄露与合规风险。

HarmonyOS 7.0(API 26)中,Input Kit 的输入法服务框架 IMF 做了架构演进:开放 InputMethodController 的拉起拦截、自定义键盘高度设置,并在系统底层引入安全键盘与密码输入的物理级、虚拟内存级隔离。下面围绕 IMF 运转逻辑,整理一套无闪烁的输入法拦截与安全隔离实现。

一、示例工程结构
  1. entry/src/main/ets/
  2. ├── components/
  3. │   ├── CustomNumberKeyboard.ets
  4. │   ├── SecurePasswordKeyboard.ets
  5. │   └── AdaptiveInputWrapper.ets
  6. ├── services/
  7. │   ├── KeyboardInterceptorService.ets
  8. │   └── SecureInputManager.ets
  9. ├── pages/
  10. │   └── TransferCheckoutPage.ets
  11. └── utils/
  12.     └── AvoidAreaCalculator.ets
复制代码

二、核心 API 与安全隔离机制
1. InputMethodController 拦截与接管
过去调用 inputMethod.getController() 后,主要监听 keyboardShow 或执行 hideTextInput()。API 26 暴露了基于回调挂起的拦截管道,可在键盘拉起请求发出后、窗口重算前介入。

2. 安全键盘与密码隔离
当 TextInput 类型为 InputType.Password 或使用新的 SecureInput 标签时,Input Kit 会在 TEE 与 ArkUI 渲染引擎整合下触发隔离:
渲染隔离:安全键盘 UI 不在应用进程空间,由系统高权限安全进程独立渲染,再通过 Surface 共享覆盖在当前 Window 上。
事件隔离:安全键盘上的触摸事件在驱动层直接分发到安全进程,应用层包括悬浮窗、无障碍服务均无法捕获按键具体坐标。
数据隔离:输入结果不会明文经 IPC 传回应用层,而是加密后传递给应用底层 Secure Context,应用层只通过特定 Token 解密验证。

三、IMF 与 WindowManager 避让逻辑
API 26 的拦截器位于 IPC 请求之后、窗口重新计算之前,因此应用上报的自定义高度会直接参与窗口避让计算。HarmonyOS 使用动态 Avoid Area 方案:WindowManager 获取键盘 Rect,判断当前焦点组件 Rect 是否与键盘 Rect 重叠;若组件不在 Scroll 或 List 中,系统通过 Matrix 变换将 ViewRoot 向上偏移;若组件处于 Scroll 容器中,系统会将避让区域大小以 onAvoidAreaChanged 事件抛给 ArkUI,由 ArkUI 调整 Scroll 底部 Padding 或 offset,使聚焦输入框位于键盘上方。

四、核心拦截服务实现
  1. import inputMethod from '@ohos.inputMethod';
  2. import window from '@ohos.window';
  3. import promptAction from '@ohos.promptAction';
  4. export class KeyboardInterceptorService {
  5.   private static instance: KeyboardInterceptorService;
  6.   private controller: inputMethod.InputMethodController = inputMethod.getController();
  7.   private isCustomKeyboardActive: boolean = false;
  8.   private customKeyboardHeightVp: number = 320;
  9.   public static getInstance(): KeyboardInterceptorService {
  10.     if (!KeyboardInterceptorService.instance) {
  11.       KeyboardInterceptorService.instance = new KeyboardInterceptorService();
  12.     }
  13.     return KeyboardInterceptorService.instance;
  14.   }
  15.   public setupInterceptor(): void {
  16.     this.controller.interceptKeyboardRequest((request: inputMethod.KeyboardRequest) => {
  17.       const inputType = request.editorInfo.inputType;
  18.       if (inputType === inputMethod.InputType.NUMBER_DECIMAL) {
  19.         request.accept(true);
  20.         this.isCustomKeyboardActive = true;
  21.         this.controller.setCustomKeyboardConfiguration({
  22.           height: this.customKeyboardHeightVp,
  23.           animationDuration: 250
  24.         });
  25.         this.notifyUIToShowCustomKeyboard();
  26.       } else {
  27.         request.accept(false);
  28.         this.isCustomKeyboardActive = false;
  29.       }
  30.     });
  31.   }
  32.   public hideCustomKeyboard(): void {
  33.     if (this.isCustomKeyboardActive) {
  34.       this.isCustomKeyboardActive = false;
  35.       this.controller.setCustomKeyboardConfiguration({ height: 0, animationDuration: 0 });
  36.       this.notifyUIToHideCustomKeyboard();
  37.     }
  38.   }
  39.   private notifyUIToShowCustomKeyboard() {
  40.     AppStorage.setOrCreate('isCustomKeyboardVisible', true);
  41.   }
  42.   private notifyUIToHideCustomKeyboard() {
  43.     AppStorage.setOrCreate('isCustomKeyboardVisible', false);
  44.   }
  45. }
复制代码

这段代码通过 interceptKeyboardRequest 判断 request.editorInfo.inputType。若为 InputType.NUMBER_DECIMAL,则 request.accept(true) 接管,并调用 setCustomKeyboardConfiguration 上报高度与动画时长;否则 accept(false) 放行。隐藏时重置 height:0、animationDuration:0,避免影响后续标准输入框。

五、输入框包装与安全模式
  1. import window from '@ohos.window';
  2. import { KeyboardInterceptorService } from '../services/KeyboardInterceptorService';
  3. @Component
  4. export struct AdaptiveInputWrapper {
  5.   @Prop placeholderText: string;
  6.   @Prop isSecureInput: boolean = false;
  7.   @Prop isAmountInput: boolean = false;
  8.   @Link inputValue: string;
  9.   @State avoidOffset: number = 0;
  10.   aboutToAppear() {
  11.     window.getLastWindow(getContext(this)).then((win) => {
  12.       win.on('avoidAreaChange', (data) => {
  13.         if (data.type === window.AvoidAreaType.TYPE_KEYBOARD) {
  14.           this.avoidOffset = px2vp(data.area.bottomRect.height);
  15.         }
  16.       });
  17.     });
  18.   }
  19.   build() {
  20.     Column() {
  21.       TextInput({ text: this.inputValue, placeholder: this.placeholderText })
  22.         .type(this.getInputType())
  23.         .height(50)
  24.         .width('100%')
  25.         .fontSize(18)
  26.         .backgroundColor('#F5F5F5')
  27.         .borderRadius(8)
  28.         .padding({ left: 16, right: 16 })
  29.         .securityMode(this.isSecureInput ? true : false)
  30.         .onChange((value: string) => {
  31.           this.inputValue = value;
  32.         })
  33.         .onBlur(() => {
  34.           if (this.isAmountInput) {
  35.             KeyboardInterceptorService.getInstance().hideCustomKeyboard();
  36.           }
  37.         })
  38.     }
  39.     .padding({ bottom: this.avoidOffset })
  40.     .animation({ duration: 250, curve: Curve.EaseOut })
  41.   }
  42.   private getInputType(): InputType {
  43.     if (this.isSecureInput) {
  44.       return InputType.Password;
  45.     }
  46.     if (this.isAmountInput) {
  47.       return InputType.NUMBER_DECIMAL;
  48.     }
  49.     return InputType.Normal;
  50.   }
  51. }
复制代码

AdaptiveInputWrapper 监听 window.avoidAreaChange,当 data.type 为 window.AvoidAreaType.TYPE_KEYBOARD 时读取 bottomRect.height 并换算 vp,作为底部偏移;securityMode(true) 用于开启安全隔离;密码类型返回 InputType.Password,金额类型返回 InputType.NUMBER_DECIMAL,普通输入返回 InputType.Normal。金额框失焦时调用 hideCustomKeyboard。

六、业务页面组装思路
业务页面可用 Stack 将自定义键盘绝对定位在底部,由 @StorageLink('isCustomKeyboardVisible') 控制显隐。示例中 CustomNumberKeyboard 以 320vp 高度配置,并通过 TransitionEffect.translate 与 250ms 动画弹出;因为拦截器已向 WindowManager 上报 320 高度,AdaptiveInputWrapper 监听到的避让高度准确,页面会自动上抬,无需手写复杂算式。密码输入框通过 isSecureInput 触发 InputType.Password,并由 securityMode(true) 进入安全隔离路径。

七、避坑指南
并发时序:不要在 TextInput 的 onFocus 里拦截并弹自定义键盘。onFocus 触发时 InputConnection 已建立,系统键盘弹出指令已发往 IMMS,此时阻止会导致系统键盘闪现后被覆盖。应使用 API 26 的 interceptKeyboardRequest 前置回调,采用回调挂起机制。
安全隔离生命周期泄漏:开启 securityMode(true) 后会分配独立隔离内存区。在自定义弹窗、半屏模态面板中使用安全输入框,销毁时若未显式 blur(),安全内存上下文可能无法及时回收,下次进入页面可能报错。建议在 aboutToDisappear 时强制释放焦点连接。
复杂嵌套避让失灵:输入框嵌套在固定高度且关闭内部滚动的容器内时,onAvoidAreaChanged 无法把内部输入框顶上来,因为外层尺寸锁死。可在避让事件中动态 translate({ y: -offset }) 整体上移包裹层,而不是只依赖 Padding 或 Scroll 自动滚动。
自定义配置重置遗漏:hideCustomKeyboard 必须重置 setCustomKeyboardConfiguration({ height: 0, animationDuration: 0 })。否则用户关闭自定义键盘后点击普通搜索框,系统仍按 320vp 预留避让空间,屏幕下方出现空白。

八、总结
HarmonyOS 7.0 Input Kit 的演进体现了从隐藏底层细节到在安全隔离前提下交还控制权的思路。interceptKeyboardRequest 斩断输入框焦点与系统键盘唤起的强耦合;setCustomKeyboardConfiguration 让自定义 UI 进入 WindowManager 避让计算;securityMode(true) 与底层安全环境把防窃听、防截图下沉到系统驱动层。合理使用这套体系,可以解决键盘遮挡与闪烁,并为安全合规场景提供坚实底座。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-13 09:23 , Processed in 0.029904 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部