恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
HarmonyOS 7 AvoidArea:折叠态表单键盘遮挡与焦点回填
首页
资讯中心
/
HarmonyOS 7 AvoidArea:折叠态表单键盘遮挡与焦点回填
HarmonyOS 7 AvoidArea:折叠态表单键盘遮挡与焦点回填
发布时间:2026/10/8 17:37:15
地址表单在直板机上看着很普通联系人、手机号、详细地址再放一个“保存地址”按钮。换到折叠设备麻烦往往不在布局第一次展开而在用户正输入手机号时把设备从展开态折到悬停态。窗口高度、系统避让区、页面可视高度和焦点树会在很短时间里连续变化。每来一次回调就立即重排页面很容易抖两下按钮被键盘盖住光标也可能跑回第一个输入框。本文用FoldFormLab处理这个细节。页面叫AddressEditPage协调器是SafeAreaCoordinator。演示任务FOLD-1818-307在 18:18 发生形态切换布局代次为307当前字段是receiverPhone。页面收到的应用侧形态标签为HALF_FOLDED根节点可视高度从 1412px 收缩键盘占用估算为 684px安全内容高度稳定在 728px。合流阶段进度显示 68%状态是WAITING_STABLE_AREA120ms 后才进入STABLE_READY。一、不要让三个回调分别决定布局折叠设备是否可折叠可以用display.isFoldable()判断当前折叠状态可以通过display.getFoldStatus()获取状态变化则由display.on(foldStatusChange)监听。窗口侧的getWindowAvoidArea()和on(avoidAreaChange)负责系统栏、挖孔区、导航指示区等避让信息。它们描述的不是同一件事。形态变化回答“设备正在怎么折”避让区回答“哪些区域不能放关键内容”页面根节点的onAreaChange回答“组件现在实际拿到了多大空间”。软键盘出现时系统默认策略通常能优先保证输入框可见却不保证输入框下面的“保存”按钮也一定露出来。这一点放到短屏或悬停态尤其明显。所以SafeAreaCoordinator不把任何单个回调当最终答案。它收集折叠状态、窗口系统避让、根节点高度和当前焦点生成同一代次的候选快照。候选值连续 120ms 不再变化才发布给页面。120ms 是 Demo 的交互预算不是系统常量真实项目要结合设备与动画节奏测量。这里还有一个容易混淆的边界HALF_FOLDED是 Demo 为日志和页面定义的业务标签不冒充display.FoldStatus的官方枚举名称。接收到的系统值先按当前 SDK 声明解释再映射成业务语义避免把不同设备形态的数值硬编码到页面。二、监听要成对第一次快照不能等事件碰巧到来第一段代码解决两个问题页面进入时主动读取初值页面离开时按同一个回调引用注销监听。只注册不注销会让旧页面继续接收形态变化只等回调而不读初值则可能在设备保持不动时一直拿不到首屏数据。import { display, window } from kit.ArkUI; import { Callback } from kit.BasicServicesKit; export class SafeAreaCoordinator { private mainWindow?: window.Window; private layoutRev: number 306; private foldStatus: display.FoldStatus display.getFoldStatus(); private foldCallback: Callbackdisplay.FoldStatus (value) { this.foldStatus value; this.enqueueCandidate(FOLD_CHANGED); }; async attach(stage: window.WindowStage): Promisevoid { this.mainWindow stage.getMainWindowSync(); this.foldStatus display.getFoldStatus(); display.on(foldStatusChange, this.foldCallback); this.mainWindow.on(avoidAreaChange, (info) { if (info.type window.AvoidAreaType.TYPE_SYSTEM || info.type window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR || info.type window.AvoidAreaType.TYPE_CUTOUT) { this.enqueueCandidate(AVOID_AREA_CHANGED); } }); this.enqueueCandidate(INITIAL_SNAPSHOT); } detach(): void { display.off(foldStatusChange, this.foldCallback); this.mainWindow?.off(avoidAreaChange); this.mainWindow undefined; this.cancelPendingCommit(); } }mainWindow.off(avoidAreaChange)在这里清理该窗口上的对应监听如果同一窗口还承载别的模块工程里应保存并传回具体回调避免一把清掉其他订阅。detach()还要取消定时提交因为页面退出后再发布STABLE_READY会把已经失效的布局状态写回新页面。首个快照主动读取getFoldStatus()系统栏和导航区则通过getWindowAvoidArea()补齐。示例没有用折叠状态直接推断窗口尺寸因为分屏、自由窗口和跨设备流转也会改变窗口空间。同一个折叠状态下可用宽高并不唯一。三、把可视高度当事实把键盘高度当推导值第二段代码解决“键盘高度没有被某个单独回调可靠交付”的问题。页面根容器通过onAreaChange报告实际高度协调器拿最近一次无键盘基线减去当前高度得到本轮的遮挡估算。估算只用于布局不写成系统事实。type AreaCandidate { rev: number; rootHeight: number; baseHeight: number; bottomSystemAvoid: number; focusedId: string; }; private pending?: AreaCandidate; private commitTimer: number -1; acceptRootArea(height: number, focusedId: string): void { const rev this.layoutRev; // 本轮为 307 this.pending { rev, rootHeight: height, baseHeight: this.lastExpandedRootHeight, bottomSystemAvoid: this.readBottomAvoid(), focusedId }; this.state WAITING_STABLE_AREA; this.progress 68; if (this.commitTimer 0) clearTimeout(this.commitTimer); this.commitTimer setTimeout(() this.commitStable(rev), 120); } private commitStable(boundRev: number): void { const c this.pending; if (!c || c.rev ! boundRev) return; const reduced Math.max(0, c.baseHeight - c.rootHeight); const keyboardPx Math.max(0, reduced - c.bottomSystemAvoid); const safeHeight Math.max(480, c.rootHeight - c.bottomSystemAvoid); this.publish({ rev: c.rev, keyboardPx, safeHeight, focusedId: c.focusedId, state: STABLE_READY }); }这段计算故意保守。根节点变矮不一定全由键盘造成折叠、窗口拖拽和系统栏变化也会参与所以日志里写“keyboardPxEstimate”而不是“系统键盘高度”。当候选代次在 120ms 内又增加旧定时器即使被调度也会被rev栅栏挡掉。safeHeight设置 480px 下限是为了避免一次瞬态零尺寸让布局坍缩。这个值同样是 Demo 的页面约束实际产品应从按钮、当前输入框、标题和错误提示所需的最小高度反推。空间低于最小值时与其继续压缩不如让表单内部滚动。工程目录分为pages/AddressEditPage.ets、service/SafeAreaCoordinator.ets、model/LayoutSnapshot.ets和components/StickySubmitBar.ets。下面的 DevEco Studio 风格图按照本文数据生成右侧模拟器停在 68% 合流阶段它是演示说明图不是真实 IDE 或真机测试证据。四、布局稳定之后再把焦点交回同一个字段折叠过程中直接调用requestFocus()往往太早。目标节点可能正在从双栏树移动到单栏树当前帧还不存在或不可见。正确顺序是保存焦点键值发布稳定布局确认新组件树已经挂载再通过当前页面的UIContext请求焦点。第三段代码解决焦点回填和提交按钮可见性。receiverPhone的 key 不随布局模式改变双栏与单栏渲染都复用同一个业务键值。Entry Component struct AddressEditPage { State snapshot: LayoutSnapshot LayoutSnapshot.initial(); State phone: string ; private scroller: Scroller new Scroller(); private restoreFocus(): void { if (this.snapshot.focusedId.length 0) return; this.getUIContext().getFocusController() .requestFocus(this.snapshot.focusedId); } build() { Column() { Scroll(this.scroller) { Column({ space: 16 }) { TextInput({ text: this.phone, placeholder: 收件人手机号 }) .key(receiverPhone) .onFocus(() this.coordinator.markFocus(receiverPhone)) // 其他地址字段 } }.layoutWeight(1) StickySubmitBar({ enabled: this.snapshot.state STABLE_READY }) } .height(this.snapshot.safeHeight) .onAreaChange((_oldArea, newArea) { this.coordinator.acceptRootArea( Number(newArea.height), this.snapshot.focusedId); }) .onDidBuild(() this.restoreFocus()) } }requestFocus()依赖目标组件已经挂树且可见。示例把请求放在新快照触发的构建完成后但仍要检查返回结果返回 false 或抛出错误时只记录诊断不循环抢焦点。用户已经主动点击别的输入框时旧代次也不能把焦点抢回去因此正式实现要把focusedId与layoutRev一起比较。03 图展示合流尚未完成的运行态任务FOLD-1818-307、代次 307、字段receiverPhone、估算键盘 684px、进度 68%、状态WAITING_STABLE_AREA。红色标注提醒此时按钮还不能根据瞬态高度固定位置。五、诊断页要把“收到事件”和“采用结果”分开如果日志只写“折叠状态变化”很难判断按钮为什么仍被挡住。Demo 的事件账本分别记录FOLD_CHANGED、ROOT_AREA_CHANGED、AVOID_AREA_CHANGED、FOCUS_RETAINED、STABILITY_TIMER和SNAPSHOT_COMMITTED。前五个是输入最后一个才是页面采用的结果。18:18:26 收到HALF_FOLDED业务标签同秒根节点变高信息与底部系统避让到达焦点仍是receiverPhone布局代次增加到 307稳定计时器重新开始。18:18:27 连续 120ms 没有新候选发布safeHeight728px、keyboardPxEstimate684px状态进入STABLE_READY。04 图专门展示这条诊断链与 03 的运行页不同。红圈落在SNAPSHOT_COMMITTED箭头说明“120ms 稳定后采用”。这些时间、尺寸和状态是演示合同不声称来自某一台真实设备的测量结果。六、异常路径比正常展开更值得测第一组测试从展开态进入悬停态键盘保持打开验证输入文本、选择区和焦点键值不丢提交栏最终可见。第二组在 120ms 内连续注入三次根高度只有最后代次 307 可以发布前两次定时任务必须被拒绝。第三组让页面在WAITING_STABLE_AREA阶段退出检查折叠与避让监听是否注销计时器是否取消。第四组模拟requestFocus()失败页面应保留输入值并显示可操作布局不能因为焦点失败阻止保存。第五组在分屏模式下重复相同折叠状态证明布局依赖实际窗口空间而不是依赖“展开/折叠”字符串。还要覆盖字体放大、错误提示变成两行、地址输入框使用输入法候选栏、导航条显示状态变化等情况。安全高度不是固定屏幕高度减固定键盘高度任何硬编码都可能在另一种字体、导航模式或窗口形态下失效。对于自定义键盘supportAvoidance: true能帮助输入框避让但官方也提醒默认能力不一定保证输入框下面的操作区可见。页面仍需根据实际可视空间决定滚动范围与提交栏位置。对系统键盘则不要套用自定义键盘的组件高度回调应该从真实窗口和页面变化建立自己的证据链。七、稳定区门禁解决的是连续性不是某一个像素值折叠态表单的关键不是找出一个“万能键盘高度”。真正需要守住的是三件事输入内容不丢用户正在编辑的字段不被无故替换保存按钮在布局稳定后可见且可操作。FOLD-1818-307在 68% 时仍处于候选合流所以页面没有提前宣称适配完成。等代次 307 的可视高度、避让区和焦点连续稳定 120ms才提交 728px 安全内容区并回填receiverPhone焦点。多等这一小段时间换来的是少一次抖动、少一次误触也少一个很难复现的“折一下按钮就没了”。实际落地时还应把稳定等待上限和无障碍体验一起纳入验收。120ms 内没有稳定快照时页面不能永久禁用提交按钮可以退化为可滚动单栏并给出明确状态屏幕朗读也不应把每次候选高度都播报出来只在最终布局采用或操作不可继续时提示。否则视觉抖动虽然消失了辅助功能用户却会听到一串没有意义的状态变化。稳定门禁的对象是布局决策不是把用户锁在等待态。参考资料折叠屏状态监听沉浸式窗口与避让区多设备通用适配指南焦点控制