ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 ArkUI + Window Kit:折叠屏编辑页的键盘避让快照与焦点恢复时序【鸿蒙心迹】

HarmonyOS 7 ArkUI + Window Kit:折叠屏编辑页的键盘避让快照与焦点恢复时序【鸿蒙心迹】 作者李游一次折叠动作之后编辑框还在光标也像是存在输入的文字却落不到原来的位置。这个问题没有崩溃堆栈单看窗口宽度和键盘高度也都正常。最后是三条相邻日志把时序暴露出来窗口先变键盘区域后变旧页面又抢了一次焦点。一、06:32光标被恢复到了“上一版页面”Demo 名叫FoldNote Lab会话编号是edit_20261001_06。展开态窗口宽度 840 vp左侧笔记列表右侧正文编辑器折叠以后宽度变为 420 vp页面切换成单栏。测试动作很简单把光标放在正文第 18 行键盘高度 312 vp 时折叠设备然后继续输入。现象不是每次都出现。慢慢折叠通常正常快速合上并立即打字时第一两个字符偶尔插到标题框再极端一点正文滚动到了第 18 行焦点却已经落到不可见的旧 TextArea。页面看起来只是“键盘闪了一下”实际上同时存在布局代次、键盘避让和焦点归属三个状态。第一版代码监听窗口宽度宽度小于 600 vp 就改成单栏页面onAppear里调用requestFocus(note_body)。两个动作单独都正确组合起来却有竞态旧双栏组件还没销毁新单栏组件已经创建二者都可能在自己的生命周期回调中请求焦点。本轮不再讨论列表锚点也不是跨栏拖拽。工程问题限定为折叠导致布局树重建时如何保存编辑快照、让键盘避让使用新窗口数据并保证只有当前代次能恢复note_body焦点。我把故障现场的日志按毫秒重新排了一遍。06:32:14.120收到窗口宽度变化14.137新单栏组件创建14.144旧双栏页面执行了一次迟到的onAppear14.151键盘高度先变成零14.168才稳定到 312 vp。原来的焦点请求位于14.145正好夹在错误位置。这个排序说明问题不是某个 API 返回错而是缺少能够淘汰旧事件的会话边界。为了避免测试只靠肉眼我在输入法事件后插入字符K再读取标题与正文的文本摘要。正确结果是标题摘要不变、正文第 18 行长度增加一如果只检查页面上键盘仍显示很容易漏掉字符进入错误输入框的情况。自动化断言因此同时验证 focusKey、selection 和文本变化。二、先把可恢复内容和瞬时对象分开我最开始把TextAreaController塞进 AppStorage希望新页面直接接着用。结果控制器绑定的是旧组件实例布局树重建后继续调用只会让状态更混乱。真正需要保存的是文本、选择区、滚动位置和目标焦点键控制器、窗口监听器、定时器都属于瞬时资源不能跨组件复用。下面这段代码解决“保存了文字却没有保存编辑语境”的问题。快照是纯数据可以跨布局形态传递layoutEpoch用来判断它是否仍属于当前一次窗口变化。exportinterfaceEditorSnapshot{sessionId:stringtext:stringselectionStart:numberselectionEnd:numberfirstVisibleLine:numberfocusKey:stringkeyboardHeight:numberlayoutEpoch:number}exportclassEditorSnapshotStore{privatevalue?:EditorSnapshotsave(snapshot:EditorSnapshot):void{this.value{...snapshot}}take(sessionId:string,epoch:number):EditorSnapshot|undefined{if(this.value?.sessionId!sessionId||this.value.layoutEpoch!epoch)returnconstcurrentthis.valuethis.valueundefinedreturncurrent}}take()是一次性消费而不是普通 get。新单栏页面恢复成功后旧页面即使晚到一个onAppear也拿不到同一份快照。正式项目如果需要撤销应另建编辑历史不能让生命周期快照兼任 undo 数据。快照中的键盘高度只是观测值不是新布局必须照搬的 padding。折叠前记录 312 vp是为了在新避让数据到达前保持编辑区不突跳一旦新页面测得真实区域就用新值覆盖。把旧高度长期固定下来会在横竖屏和浮动键盘场景留下空白。文本量较大时保存快照也不能每个字符都复制整篇正文。当前 Demo 在布局事务开始时读取一次最终文本平时编辑由页面状态持有如果正文来自文档模型则快照只保存文档版本号与 selection恢复时从同一个版本取内容。版本已经变化就放弃自动 selection 恢复避免把旧光标位置套在新文本上。选择区需要同时处理中文输入法的组合态。用户正在拼音候选阶段折叠设备时尚未提交的组合文本不一定属于普通 selection。Demo 的策略是先结束当前输入会话再捕获已提交内容正式编辑器若要无感保留组合态需要输入法能力提供明确支持不能自行拼接候选字符串。这里宁可少保留一段未确认输入也不制造重复文字。三、窗口变化只开启事务不立即抢焦点工程目录按事件来源拆分window负责宽度与 epochkeyboard负责区域测量editor负责快照页面只订阅聚合后的FoldEditState。entry/src/main/ets ├── pages/FoldNotePage.ets ├── components/SplitEditor.ets ├── components/StackEditor.ets ├── editor/EditorSnapshotStore.ets ├── keyboard/KeyboardAvoidCoordinator.ets ├── window/LayoutEpochController.ets └── model/FoldEditState.ets第二段代码解决“窗口事件一到就切布局焦点恢复早于新组件挂载”的问题。控制器把一次折叠处理成事务先递增 epoch 并保存快照再更新布局模式等新组件报告 ready 后才允许恢复。asynconWindowWidthChanged(widthVp:number):Promisevoid{constnextModewidthVp600?LayoutMode.SPLIT:LayoutMode.STACKif(nextModethis.state.mode)returnconstepochthis.state.layoutEpochthis.state.phaseEditPhase.SNAPSHOTTINGthis.snapshots.save(this.editor.capture({sessionId:edit_20261001_06,keyboardHeight:this.keyboard.height,layoutEpoch:epoch}))this.state.focusEnabledfalsethis.state.modenextModethis.state.widthVpwidthVpthis.state.phaseEditPhase.WAITING_LAYOUTawaitthis.layoutReady.wait(epoch)awaitthis.restoreFocus(epoch)}这里先关闭focusEnabled避免组件自动聚焦。layoutReady.wait(epoch)不是固定延时页面根节点onAreaChange确认尺寸稳定后以 epoch 回报 ready。固定setTimeout(100)在一台设备上可能有效换到动画时长不同的设备又会复现。重复窗口回调也是边界。折叠过程中宽度可能连续变化只要布局模式仍是STACK就不新建事务若模式再次变化旧 epoch 的等待会被判定过期。这样 840→590→420 vp 只触发一次模式切换不会保存三份互相覆盖的快照。layoutReady的完成条件也不是“组件已经构建”这么宽泛。它要求根节点宽度与目标模式一致、编辑器可见区域高度大于零并且本次回调携带当前 epoch。组件构建结束但尺寸仍是旧值时提前放行后面的scrollToLine(18)仍会按双栏高度计算。把 ready 定义成可测量状态比在生命周期名称上猜测更稳定。四、键盘避让需要接受“先零、后真实值”新页面刚挂载时自定义键盘根节点可能先上报高度 0随后才到 312 vp。如果把 0 当作最终值正文会先铺到底部再被键盘顶起同时请求焦点光标滚动就可能以错误的可视高度计算。这里没有把 Window Kit 的系统栏避让和自定义键盘高度混成一个数。状态栏、导航区域来自窗口避让信息键盘高度来自键盘组件onAreaChange。最终底部 padding 取键盘高度与导航区域的合理组合并保留最小业务间距。下面这段代码解决“旧 epoch 的键盘回调污染新布局”。协调器只接受当前 epoch并在连续两次稳定测量后发布隐藏键盘时则允许零立即生效。exportclassKeyboardAvoidCoordinator{height:number0privatecandidate:number-1privatehits:number0update(measuredVp:number,epoch:number,currentEpoch:number):boolean{if(epoch!currentEpoch)returnfalseif(measuredVp0){this.height0this.candidate-1this.hits0returntrue}if(Math.abs(measuredVp-this.candidate)1){this.hits}else{this.candidatemeasuredVpthis.hits1}if(this.hits2)returnfalsethis.heightmeasuredVpreturntrue}}连续两次不是通用真理而是这个 Demo 对自定义键盘动画的工程选择。系统键盘、浮动键盘和分离键盘可能需要不同策略正式项目应以实际事件源为准。关键是把“测量候选”和“已提交避让值”分开而不是每个瞬时高度都驱动整页重排。高度变为零时立即发布是为了收键盘后不残留大块空白但页面正在折叠事务中时零值仍带 epoch 校验。监听必须在组件消失时注销否则旧SplitEditor会继续把自己的 312 vp 写进StackEditor。窗口系统栏的避让区域还存在高度为零的合法情况例如系统元素隐藏或某些窗口形态。代码不能把零一律当异常也不能继续沿用上一帧系统栏高度。键盘候选需要稳定策略是因为它参与动画系统栏则按当前窗口返回值更新。把不同来源放进同一个“非零才更新”函数正是早期版本留下底部空白的原因。列表与编辑器并排时右侧编辑器的可用高度由窗口安全区域和键盘共同决定左侧列表却不必跟随键盘上移。折叠成单栏后两者变成同一页面策略随布局模式改变。这也是协调器输出业务语义editorBottomInset而不是向所有组件广播一个全局bottomPadding的原因。开发截图中左侧展开的是editor / keyboard / window三组目录中间代码停在restoreFocus(epoch)的代次判断右侧模拟器显示FoldNote Lab单栏编辑页底部日志依次是sessionedit_20261001_06、width840-420vp、modeSPLIT-STACK、keyboard312vp、caretLine18、epoch7、restore34ms stateFOCUS_RESTORED。五、恢复焦点的最后一步必须再次检查代次layoutReady返回以后页面先恢复文本和选择区再把第 18 行滚进可视范围最后通过当前 UIContext 的焦点控制器请求note_body。顺序不能反过来如果先请求焦点键盘弹起会触发一次滚动随后再恢复选择区又会触发第二次滚动用户看到的就是跳动。恢复前还要再次比较 epoch因为等待布局期间可能发生第二次窗口变化。只有epoch state.layoutEpoch、页面仍可见、会话 ID 一致时才执行。请求焦点返回成功也不代表编辑器一定可输入测试里还会检查当前 focusKey 和 selection二者都正确才把状态改为FOCUS_RESTORED。生命周期上页面aboutToAppear注册窗口和键盘监听aboutToDisappear先递增 epoch 让所有等待失效再注销监听、取消 ready Promise。若只是弹出临时菜单不销毁编辑页面就不应该清空快照。资源释放的判断依据是组件是否真正离开而不是视觉上是否被遮挡。我还保留了失败路径34 ms 内没有等到布局稳定时状态进入RESTORE_DEFERRED文本不会丢页面也不强行抢焦点用户下一次点击编辑器即可自然恢复。工程上宁可少一次自动聚焦也不要在页面不可见时把键盘重新拉起来。恢复成功后不会立即删除所有诊断信息。最近一次 session、epoch、目标焦点、恢复耗时和失败原因保留到本次编辑会话结束便于用户反馈后导出文本内容与输入字符不进日志只记录长度和摘要。这样既能还原时序又不会把笔记正文写入调试文件。连续折叠测试跑了五十次其中十次在键盘动画中反向展开。修复前出现七次焦点漂移和三次底部空白修复后五十次都满足标题不变、正文字符增加、旧 epoch 无写入。这个数字只代表当前设备组合不是跨设备结论因此测试报告仍保留设备形态和输入法版本。六、运行结果要能证明“输入落在正确位置”06:32 的运行页显示窗口840 → 420 vp、布局SPLIT → STACK、键盘 312 vp、光标第 18 行、焦点键note_body、epoch 7、恢复耗时 34 ms最终状态FOCUS_RESTORED。底部保留一行刚输入的验证文本确认字符落在正文而不是标题框。红色批注只指向“epoch 7”和“焦点已恢复”。这两个字段分别证明旧回调被隔离、当前编辑器获得输入权。手机图是折叠后的单栏结果不再额外塞一张展开态对比避免把运行截图做成拼图。七、这类问题不是布局宽度问题而是事件所有权问题回头看窗口宽度、键盘高度和光标行号都没有算错。错误发生在三个合法事件争抢同一个结果旧页面想恢复焦点新页面想建立布局键盘动画想调整可视区域。没有 epoch 时每个回调都以为自己仍然有效。FoldNote Lab最后采用的判断很克制窗口变化开启事务快照只保存纯数据键盘测量先稳定再提交焦点恢复永远在布局和选择区之后并在执行前后核对当前代次。它没有试图让所有设备共享一个延时常数也没有把控制器跨页面搬运。正式产品还要覆盖外接键盘、分屏自由窗口、输入法切换和进程重建。外接键盘可能没有 312 vp 避让区但焦点恢复仍然需要进程重建只能从持久化文本恢复不能恢复旧控制器。测试矩阵应至少包含展开到折叠、折叠到展开、键盘显示与隐藏四种组合并加入快速连续切换。这次排查让我更在意一条日志是否能说明“谁拥有当前页面”。stateFOCUS_RESTORED前必须同时带 session、epoch 和 focusKey缺一项都可能只是偶然成功。页面不跳、文字不丢只是表象旧事件无法修改新页面才是这次修复真正守住的边界。参考资料窗口沉浸式与 AvoidArea 动态监听自定义键盘与布局避让多设备通用适配指南
返回列表