HarmonyOS 7 ArkUI customKeyboard:折叠屏底栏避让去抖【鸿蒙心迹】
一个编辑器的输入框能跟着键盘抬起来,不代表底部的“保存草稿”和“插入图片”也安全了。折叠屏窗口从紧凑形态变成展开形态时,这个差别更明显:文字光标没有被挡住,但固定底栏可能压进输入法区域;如果页面同时响应系统避让与业务补偿,还可能把底栏推得过高。最后看到的不是漂亮的自适应,而是按钮在屏幕上跳了两次。
本篇只讨论一件具体的事:ArkUI 自定义键盘已经启用输入框避让时,业务底栏应该如何依据当前窗口、键盘高度与安全区做第二层规划,以及如何拒绝重复、迟到的布局回调。工程示例是 KeyboardDockPlan,数据来自六条固定输入夹具,没有在折叠屏实机上弹起过键盘。用状态和数学合同把问题先做透,再安排设备验证。

一、输入框可见与操作栏可点不是同一个承诺
HarmonyOS ArkUI 的 TextInput 可以通过 customKeyboard 绑定自定义键盘。华为开发者文档给出的选项 { supportAvoidance: true },用于开启系统提供的自定义键盘避让能力。官方同时提醒,这类默认避让主要保证输入控件本身不会被挡住,输入框下方的其他控件仍可能被键盘覆盖。这个限定很重要:它已经直接说明,不能把系统替我们处理输入框的结果等同于业务底栏也处理好了。
示例编辑器里输入区域上方有照片描述,输入区域下方有三个操作:“保存草稿”“插入图片”“查看避让诊断”。操作栏高度为64vp,用户在折叠屏展开态打字时,它应停留在键盘上沿之上,同时与输入法保持一定距离。简单地给页面写死一个 bottom=300vp,看起来能解决当前设备,换成横屏、较小键盘或者键盘隐藏就会出现大段空白。
更难处理的是事件乱序。折叠态窗口先发来宽度变化,键盘测量随后才到,操作栏可能先按旧窗口算一遍,再按新键盘算一遍。对用户而言,这两次跳动发生在同一个瞬间,哪怕最终坐标正确,也已经造成误触。业务层需要把几何输入和“当前布局轮次”绑定起来,旧轮次回调不再有资格修改最新位置。
因此工程验收不能只盯 TextInput 是否在屏幕内。需要同时检查输入光标、底栏上沿、底栏下沿、键盘上沿、系统安全区以及按钮实际点击区。文中只实现业务坐标规划,未宣称自定义键盘在具体型号上已经满足这些触点和无障碍要求。
二、固定一个能推导出结果的展开态
这一轮的任务编号固定为 KEY-1011-28,数据集为 editor_insets_06。选中的第三个场景 S03 代表折叠屏展开形态,窗口宽720vp、高880vp,键盘高度300vp,底部安全区24vp,操作栏高度64vp,业务规定的最小额外间距12vp。输入数据来自应用自定义布局模型,不是从设备屏幕实时采集的系统原始值。
根据这组数字,业务预留空间是300+24+12=336vp。底栏顶部位置为880-336-64=480vp,底栏下沿为544vp,键盘上沿为880-300=580vp,因此二者之间有36vp的间隙。这里36vp不是从视觉截图量出来的结果,而是业务规则推导出来的结果:24vp安全底部与12vp额外间距合计保留36vp。
为什么不直接把底栏贴到键盘上沿减去12vp?因为安全区可能表示设备交互边界,不能轻易将其重复或者忽略。当前示例刻意采用保守模型:无论键盘是否覆盖部分系统安全区,都先把安全区与最小间距作为可见界面的安全预算,宁可多留一点空白。正式设备适配必须先确认收到的高度已经包含了哪些系统占用,不能把同一个区域扣减两次。
同一公式用于四个有效场景:S01 紧凑竖屏390×780vp、键盘0vp、安全区24vp,得到底栏顶部680vp;S02 紧凑竖屏390×780vp、键盘286vp,得到394vp;S03 展开态720×880vp、键盘300vp,得到480vp;S04 横向窗口880×720vp、键盘264vp、安全区20vp,得到360vp。四组输入共同验证了公式能随着窗口变化,而不是只靠写死一个常数。
这并不是说所有 HarmonyOS 设备的键盘高度都会是这些数。各厂家键盘、悬浮键盘、第三方输入法以及分屏窗口都可能改变实际可用区域,所以绝不能用固定样例去反推系统规范。当前四组场景仅是程序可复算的输入边界,用来驱动模型测试和示意图片。
三、先把系统避让和业务避让分层
页面层建议只负责两个动作:为输入控件配置 customKeyboard,将键盘根节点的尺寸变化转交业务模型。真正决定底栏位置的函数不应散落在 TextInput、Button 和父 Column 的多个回调里。只要有一个唯一入口负责综合窗口尺寸和键盘高度,重复调整的机会就会少很多。
下面代码展示了 ArkUI 接入点。官方文档确认 TextInput 的 customKeyboard 绑定方式和 supportAvoidance 选项;业务 onKeyboardArea 是项目自定义方法,并不属于系统 API。为避免把不完整的页面上下文写成可直接编译的完整 Demo,代码只呈现与本问题有关的结构性片段;集成时需要接入真实的 @State、窗口尺寸来源和组件生命周期。
@Entry
@Component
struct ComposePage {
@State inputText: string = '今天整理的旅行照片';
@State dockTop: number = 480;
@State keyboardHeight: number = 300;
@Builder
inputPad() {
Column() {
Text('自定义输入区(示意)')
}
.onAreaChange((oldValue: Area, newValue: Area) => {
this.onKeyboardArea(Number(newValue.height));
})
}
private onKeyboardArea(height: number): void {
this.keyboardHeight = Math.max(0, height);
// 此处把当前高度交给应用自定义 InsetPlanner;不直接叠加偏移。
}
build() {
Column() {
TextInput({ text: this.inputText, placeholder: '写点什么' })
.customKeyboard(this.inputPad(), { supportAvoidance: true })
Text('底部操作栏由单一业务坐标模型控制')
}
}
}
有两个边界要及时说明。第一,onAreaChange 会在组件面积发生变化时触发,它并不是“所有设备输入法已经完整弹出”的业务承诺,仍需考虑键盘隐藏与横竖屏切换。第二,示例是自定义键盘,不应直接推导到系统默认输入法的全部回调顺序。正式项目如果同时支持系统键盘和自定义键盘,要明确当前输入源,避免自定义键盘高度与系统键盘高度相互覆盖。
组件退出时还要处理订阅释放。若选择用 emitter 把键盘测量广播给页面,必须在组件不再使用时取消对应监听;若改用自有状态对象,也要阻止旧页面继续推送高度。仅在 aboutToDisappear 将 dockTop 归零,并不能消除已经排队的异步事件。状态退场需要独立的代次校验。
四、计算公式之外,要有回调接纳规则
坐标计算越简单,工程里越容易忽略事件身份。固定夹具总共安排六次回调:S01 至 S04 是四次有效几何变更;S05 是重复参数事件,应直接忽略;S06 来自布局旧轮次 epoch2,也应忽略。当前有效轮次为 epoch3。这样总回调六次、采纳四次、重复忽略一次、过期忽略一次,正好能够和诊断界面中的计数一一核对。
业务状态 DOCK_CLEAR 的含义也必须受限。它表示按照输入夹具的几何条件,目标底栏的位置不会撞进模型键盘区域。它不意味着触摸事件路由已经通过真机验证,不意味着系统安全区一定采用本文假设的坐标系,更不意味着所有第三方输入法都被完整兼容。
关键代码采用独立的纯 TypeScript 规划器。它先校验 epoch,再利用几何签名拒绝重复,最后才计算 top。这里的 geometryKey 必须包含窗口高、键盘高、安全区、操作栏高度和间距;仅比较键盘高会漏掉“旋转后键盘相同而可用高度不同”的场景。
interface DockEvent {
id: string; epoch: number; width: number; height: number;
keyboard: number; safeBottom: number; barHeight: number; minGap: number;
}
interface DockResult { status: string; top: number; reserve: number }
class InsetPlanner {
currentEpoch: number = 3;
lastKey: string = '';
applied: number = 0;
duplicateIgnored: number = 0;
staleIgnored: number = 0;
apply(e: DockEvent): DockResult {
if (e.epoch !== this.currentEpoch) {
this.staleIgnored += 1;
return { status: 'STALE', top: -1, reserve: -1 };
}
const key = [e.width, e.height, e.keyboard, e.safeBottom,
e.barHeight, e.minGap].join(':');
if (key === this.lastKey) {
this.duplicateIgnored += 1;
return { status: 'DUPLICATE', top: -1, reserve: -1 };
}
this.lastKey = key;
const reserve = Math.max(0, e.keyboard) + e.safeBottom + e.minGap;
const top = Math.max(0, e.height - reserve - e.barHeight);
this.applied += 1;
return { status: 'APPLIED', top, reserve };
}
}
这里用 -1 表示未应用的占位返回值,只允许诊断模型内部消费,正式 UI 不可将其当成有效坐标。对负的窗口尺寸、超出窗口高度的键盘高度、NaN 等异常输入,还应增加输入合法性检查,避免 Math.max 掩盖了上游测量错误。样例只覆盖正常数值与重复、过期两种业务异常,不把边界检查缺失隐藏起来。
需要留意 lastKey 的重置时机。当 app 从后台回来,或者用户切换了编辑器文档,历史签名可能已不再适用。如果继续用旧签名,第一次恢复画面的有效更新会被误当成重复。稳妥做法是将签名与窗口会话 ID、页面代次一起保存,并在新的会话建立时重置。这样既保留回调去抖能力,也不会把上一次编辑的布局偏移遗留给新的页面。
五、为什么只报告三次“优化前遮挡”
本次夹具事先为 S02、S03、S04 设定了未执行二次避让时的潜在底栏碰撞。S01 键盘未弹起,不应算作遮挡。S05 和 S06 分别是重复与过期事件,不参加最终几何碰撞统计。因而业务模型给出的优化前碰撞数是三,优化后是零。这里的数字来自测试输入集合的比较,而不是通过截图或自动化点击实际测得的遮挡次数。
这个口径不能偷换成“设备上误触率从三降到零”。误触与遮挡不是一回事:视觉上不相交的底栏可能仍有透明触摸区域覆盖键盘,按钮也可能被系统手势从底部拦截。实际验收时必须增加触控热区、焦点移动、无障碍最小触摸目标及长按手势的检查。否则几何规划虽然通过,体验仍然可能不稳定。
展开态 S03 的验证更直观:底栏顶部480vp,加64vp高度得到下沿544vp;键盘上沿580vp,相差36vp。这个36vp不是“还有36vp可以随便填内容”,其中已经预留安全区24vp和附加间距12vp。页面设计图应把它标为系统与业务共同考虑的空间,不应再让产品经理在同一区域临时放一个悬浮发送按钮。

图中的 DevEco Studio 白色主题、右侧 HarmonyOS 模拟器以及底部 HiLog 都属于生成式开发示意图。它用来说明文件组织、参数传递和日志字段;不能作为真实 DevEco 编译通过或键盘设备实验的证据。图中 ComposePage.ets、AvoidAuditPage.ets 和 InsetPlanner.ets 分别代表输入页、诊断页与业务规划器。页面内任何数值都沿用 KEY-1011-28 合同。
六、编辑器主页面如何解释一个保守的位置
ComposePage 不是单纯的数字面板,它包含一个描述旅行照片的输入区域和底部草稿操作栏。展开态画面显示窗口720×880vp、键盘高300vp、安全区24vp、操作栏高64vp、最小间距12vp、预留336vp、操作栏顶部480vp、键盘顶部580vp、实际间距36vp。用户可以在观察图中直接看到“为什么按钮不贴着键盘”。
另外,主页面必须能够暴露问题来源。倘若用户反馈底栏在某个折叠角度被遮住,诊断页面至少要回传任务ID、当前布局轮次、最后一次被采纳的窗口尺寸、键盘来源、重复事件统计以及旧轮次拒绝理由。只拍一张编辑界面截图,工程师很难判断是系统避让没生效,还是业务层自己补偿过度。

截图是 9:16 比例的纯手机UI示意,没有设备外壳;完整状态栏并不表示屏幕来自真实设备。页面底部画出了键盘,是为了帮助理解业务规划中键盘与操作栏的空间关系,不代表自定义键盘已真实接入输入法服务,也不表示其键帽高度与具体硬件键盘相同。
七、用固定夹具对齐六个事件与两个拒绝原因
为避免只靠人眼检查,可以用 Node.js 执行与 ArkTS 模型同构的计算断言。这个测试并不需要系统 UI。它验证的是四组预期 top 值、最后一次展开态坐标、重复回调与旧轮次回调不会增加 applied 计数。测试中 S01 和 S02 的窗口大小相同,但键盘高不同;S03 与 S04 同时改变窗口形态和键盘高,专门防止只看一个参数的去重策略。
import assert from 'node:assert/strict';
const make = (id, width, height, keyboard, safeBottom, epoch = 3) => ({
id, width, height, keyboard, safeBottom, epoch, barHeight: 64, minGap: 12
});
const events = [
make('S01', 390, 780, 0, 24),
make('S02', 390, 780, 286, 24),
make('S03', 720, 880, 300, 24),
make('S04', 880, 720, 264, 20),
make('S05', 880, 720, 264, 20),
make('S06', 720, 880, 300, 24, 2)
];
const planner = new InsetPlanner();
const results = events.map(e => planner.apply(e));
assert.deepEqual(results.slice(0, 4).map(r => r.top), [680, 394, 480, 360]);
assert.equal(planner.applied, 4);
assert.equal(planner.duplicateIgnored, 1);
assert.equal(planner.staleIgnored, 1);
assert.equal(880 - 300 - (480 + 64), 36);
在实现这段测试时还有个容易遗漏的问题:计算最后的 clearance 时,应使用 S03 的宽高数据,而不是默认当前界面已经移动到 S04。因为 S04 是另一个横屏场景,界面上选中的场景仍是 S03。诊断数据和示意图可以选一个状态展示,但事件序列需要完整保留,否则把不同时间点的数据拼到同一面板,就会制造看似无法解释的公式偏差。
本轮约定的诊断时间线为 02:41:11 初始化、02:41:12 键盘高度300vp、02:41:13 窗口展开720×880vp、02:41:14 横屏场景880×720vp、02:41:15 忽略重复事件、02:41:16 忽略 epoch2 的旧回调。它是人工设计的模型事件序列,正式程序不能把这些时间戳写死;真正的日志应记录设备时间、单调计数器和页面会话 ID。

八、不会被坐标公式覆盖的体验问题
自定义键盘还有输入控制、光标、候选词、辅助功能和系统键盘切换问题。本文的几何合同没有触碰这些功能,不能因为坐标看起来正确,就假定中文拼音候选、长文本选择或第三方输入法都工作正常。尤其在无障碍开启时,焦点移动会让页面滚动,滚动容器和绝对定位底栏的关系需要额外验收。
键盘弹出和窗口变化若在同一帧发生,还要决定 UI 更新策略。过度使用节流可能让按钮暂时停在危险区域;完全不限制更新又会导致抖动。本文采用相同几何签名去重并拒绝旧 epoch,先保证不接受无意义或过期变化,再由真正的页面层考虑动画和同帧合并。视觉动画绝不能覆盖安全判定:只要处于未知或不安全位置,按钮的危险操作应暂时不可触发。
资源释放同样不可忽略。如果自定义键盘使用了独立组件监听器,在页面关闭、编辑任务结束或应用转后台后,应清理当前会话的监听与状态。若事件从外部控制器异步转入页面,旧回调返回后只可写日志,不得再修改已销毁组件的 @State。回到前台时新建 epoch,并用新窗口和键盘测量重新计算,不把旧坐标当作可信的最终结果。
还有一个产品层取舍:紧凑态不一定需要始终固定底栏。若屏幕可用高度过小,强行显示三枚按钮可能挤压正文,让编辑器无法完成最基本的输入。此时可以将低频操作折叠到菜单,保留保存入口,或者在键盘打开时采用轻量的单按钮状态。本文只证明几何预算模型可复算,不决定产品最终采取哪种交互形态。
还有一种情况不能靠简单的高度差解决:输入法本身可能以悬浮面板显示,没有覆盖窗口底部。此时把整个键盘高度当作底部占用会错误地顶起操作栏,造成大片无意义的空白。正式适配时应使用系统提供的实际避让区域而非盲目复用本模型的矩形高度;如果平台只返回键盘内容尺寸,还需要结合窗口坐标与键盘位置进行交集计算。本文没有模拟悬浮键盘,所以无法对这种特殊形态给出通过结论。
最后应把真实设备验收拆成视觉与交互两套证据。视觉侧抓取窗口几何、输入区域和底栏矩形,检查遮挡与安全间距;交互侧记录点击命中率、焦点移动、语音输入、选词窗口和系统返回手势。即便几何结果通过,只要焦点切换时底栏突然抢走输入光标,依然应判为未完成适配。所有验收需要注明设备形态、系统版本、输入法、字体缩放和横竖屏状态,不能把一张模拟器截图当成通用结论。
九、这一轮能交付的是明确的待验证边界
固定模型得到六次回调、四次应用、一次重复忽略、一次过期忽略;优化前定义的碰撞场景三次,应用业务避让模型后为零。展开态采用的操作栏顶部480vp,键盘顶部580vp,两者保留36vp间距,结果记为 DOCK_CLEAR。这些结论只对给定输入数据成立,不意味着已经在真实折叠屏上通过输入法、系统手势或可访问性测试。
本轮可在官方文档中核对的是 ArkUI TextInput.customKeyboard 与 supportAvoidance 机制,以及通过 onAreaChange 取得自定义键盘根节点区域尺寸的开发思路。业务 InsetPlanner、回调 epoch、碰撞统计和 DOCK_CLEAR 都属于应用自定义模型,不是 HarmonyOS 提供的系统状态名。后续应在具体SDK下编译 ArkTS、核实 safeArea 的坐标空间,再使用真实设备分别测试紧凑态、展开态、横向窗口、系统与第三方输入法,记录触控坐标与实际 keyboard-inset 回调。
如果要把这套思路放入真正的项目,我更愿意先留一条严格的线上诊断路径:当布局不安全时冻结危险按钮,记录可复原的几何输入和代次,等待下一次可确认的测量;而不是为了画面永远贴底,把未知尺寸强行当作零。把“输入框能看见”和“底栏能安全使用”分别验收,才是折叠屏编辑场景里需要的工程边界。
官方资料:华为《自定义键盘》(2026-09-14),https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-customize-keyboard 。其中 customKeyboard、supportAvoidance 与 onAreaChange 的说明用于限定本文的系统能力;其余避免策略均由应用自行定义。
更多推荐




所有评论(0)