HarmonyOS 7 Window Kit + ArkUI Focus:闪控窗形态切换中的输入焦点重建与软键盘避让【鸿蒙心迹】
闪控窗 Demo 做到“能展开、能收起”以后,我原本以为输入框已经没什么问题。真正放到搜索类场景里才发现,窗口从完整形态切到紧凑形态,再恢复回来时,TextInput 虽然还在,焦点却不一定还属于它;更麻烦的是键盘已经弹出时切换形态,窗口高度变化和 IME 避让会同时发生,页面可能连续上移两次。
我这次没有继续研究窗口动画,而是单独处理焦点和软键盘:记录最后一个输入组件,形态稳定后再申请焦点,用 generation 丢掉过期请求,同时把键盘高度和窗口可用高度统一算成一次避让。
Demo 名称是 QuickFocusLab。
一、先把“窗口可见”和“输入可编辑”拆开
这次最终数据固定为:
- Session:
quick_focus_20261001_13 - Window Mode:
FLOATING - State:
FOCUS_RESTORED - Input ID:
search_input - Focus Restore Count:
2 - Keyboard:
VISIBLE - IME Height:
356 vp - Avoid Offset:
228 vp - Shape Switches:
3 - Dropped Focus Requests:
1 - Last Switch:
13:56:44
我最初只用 windowVisible=true 判断是否恢复焦点,这个条件太粗。窗口可见时,ArkUI 组件树可能还没完成当前形态的布局,立刻 requestFocus() 有时会失败,有时会先拉起键盘再触发第二次布局。
所以我把状态分成 shapeStable / keyboardVisible / lastFocusId / focusGeneration 四部分。
二、形态切换时先作废旧焦点请求
第一段代码解决的是重复申请焦点。
private focusGeneration: number = 0
private lastFocusId: string = 'search_input'
@State shapeStable: boolean = false
private onShapeChanging(mode: string): void {
this.shapeStable = false
this.focusGeneration++
this.windowMode = mode
}
private onShapeStable(): void {
this.shapeStable = true
const generation = ++this.focusGeneration
this.scheduleRestoreFocus(generation)
}
每次窗口开始切换,我先递增 generation。这样上一次 setTimeout、动画结束回调或页面更新回调即使晚到,也会被识别成旧请求。
本次一共发生 3 次形态切换,最终统计里 Dropped Focus Requests=1,说明确实有一个旧请求被主动丢弃。
三、焦点恢复放在形态稳定以后,而不是窗口事件刚到就执行
ArkUI 提供焦点控制能力,我在组件上给输入框固定 id:
TextInput({
placeholder: '输入任务名称'
})
.id('search_input')
.onFocus(() => {
this.lastFocusId = 'search_input'
})
真正恢复时,再使用当前 UIContext 对应的 FocusController:
private scheduleRestoreFocus(
generation: number
): void {
this.getUIContext().getPromptAction()
setTimeout(() => {
if (generation !== this.focusGeneration) {
this.droppedFocusRequests++
return
}
if (!this.shapeStable) {
return
}
const ok = this.getUIContext()
.getFocusController()
.requestFocus(this.lastFocusId)
if (ok) {
this.focusRestoreCount++
this.state = 'FOCUS_RESTORED'
}
}, 80)
}
这个 80 ms 不是系统要求,而是 Demo 的形态稳定缓冲。正式项目里更好的做法是接入窗口形态完成回调或页面布局完成信号,避免依赖固定时间。
这里最关键的是“只恢复最后一个真实获焦过的组件”。如果用户之前点的是筛选按钮,就不应该每次恢复窗口都强行把焦点抢回搜索框。
四、键盘避让不要和窗口缩放分别算
键盘已经显示时,闪控窗的尺寸变化会改变页面可用高度。如果业务还单独按照旧窗口高度做一次 translateY,很容易产生双重避让。
我在 Demo 中把键盘区域统一换算成一个 avoidOffset:
private updateAvoidOffset(
imeHeight: number,
inputBottom: number,
windowHeight: number
): void {
const safeBottom = windowHeight - imeHeight
const overlap = inputBottom - safeBottom
this.avoidOffset = Math.max(
0,
Math.min(overlap + 12, 260)
)
}
最终运行时 IME Height 是 356 vp,计算后的 Avoid Offset 是 228 vp。
260 是这个 Demo 给内容区设置的最大业务避让值,不是系统常量。正式产品应该结合当前窗口高度、底部操作栏和内容最小可见高度动态决定。
五、自定义键盘场景更容易遇到“先获焦、后渲染”
如果页面使用 customKeyboard,我不会在修改 keyboardVisible 的同一帧立刻通过当前帧 FocusController 强抢焦点。更稳妥的方式是先让自定义键盘完成渲染,再在后续帧恢复输入焦点。
这类问题的根源不是键盘 API 本身,而是“组件树状态变化”和“焦点转移”发生在不同时间点。
所以我的顺序是:
SHAPE_CHANGE → UI_REBUILD → KEYBOARD_READY → FOCUS_RESTORE
而不是把所有调用都塞进一个窗口回调里。
六、项目里我专门做了一个焦点协调层
entry/src/main/ets/
├─ pages/QuickFocusPage.ets
├─ focus/FocusRestoreCoordinator.ets
├─ window/QuickWindowStateBridge.ets
└─ keyboard/KeyboardAvoidCoordinator.ets
页面只声明输入框和 UI;QuickWindowStateBridge 把闪控窗形态变化转换成业务状态;FocusRestoreCoordinator 只管 generation 和焦点恢复;KeyboardAvoidCoordinator 只管 IME 高度和避让距离。
HiLog 里我保留:
shape switch=3 mode=FLOATING
drop stale focus generation=2
requestFocus id=search_input ok=true
keyboard=VISIBLE imeHeight=356vp
avoidOffset=228vp
State: RESTORING -> FOCUS_RESTORED
七、最终结果要同时看焦点和页面位置
手机运行图里,我会把输入框、键盘状态和避让量同时展示出来。
最终 Focus Restore Count=2,说明三次形态切换里有两次确实需要恢复输入焦点;Dropped Focus Requests=1 证明过期请求没有继续抢焦点;Keyboard=VISIBLE 时 Avoid Offset=228 vp,输入区仍然位于可见区域。
如果只看“光标还在”,其实无法判断页面是不是被多避让了一次。
八、生命周期里最容易忘的是注销监听
窗口尺寸、键盘区域和页面焦点通常都会注册监听。页面退出或窗口销毁时,必须按注册位置成对注销。
尤其是焦点协调器如果被重复创建,旧 listener 还在,就会出现一次形态切换触发两次 requestFocus()。这类问题表面看起来像系统焦点抖动,实际上是业务监听重复。
正式项目里我会让 Coordinator 跟页面或窗口实例绑定,attach() / detach() 成对出现。
九、这次的判断标准不是“键盘能弹出来”
闪控窗的输入体验真正稳定,需要三件事同时成立:
- 形态变化过程中不抢焦点;
- 新形态稳定后只恢复最后一个有效焦点;
- 键盘避让只计算一次,不和窗口缩放叠加。
最后我把状态链路定成:
SHAPE_SWITCHING → LAYOUT_READY → RESTORING → FOCUS_RESTORED
窗口形态本身只是外壳,焦点、键盘和页面布局才是用户真正能感知到的交互连续性。把这三条时间线分开管理以后,闪控窗从紧凑形态恢复回来,输入框才不会出现“看起来还在,实际上已经失去编辑上下文”的情况。
更多推荐



所有评论(0)