【OpenHarmony/HarmonyOs 】58|沉浸式页面不是铺满屏幕:安全区、键盘避让与输入体验完整实战

沉浸式布局的目标不是让内容机械地延伸到状态栏和导航条下面,而是在充分利用屏幕空间的同时,保证标题、按钮、输入框和消息列表始终可见、可点、可读。本文以 HarmonyOS NEXT 的 ArkUI 页面为背景,系统梳理窗口安全区、软键盘避让、聊天输入区联动、弹层叠加和多设备验证方法。

一、为什么沉浸式页面最容易在输入场景翻车?

普通静态页面只需要考虑顶部与底部是否被系统区域遮挡。

一旦页面包含输入框,布局会同时受到以下因素影响:

  • 状态栏高度与显示状态;
  • 底部导航区域或手势区域;
  • 软键盘弹出后的可用窗口高度;
  • 横竖屏切换;
  • 刘海、挖孔与圆角屏;
  • 自定义底部工具栏;
  • 表情、附件、更多面板;
  • 弹窗与半屏模态层;
  • 字体缩放和无障碍设置;
  • 手机、平板和 2in1 的窗口差异。

因此,沉浸式输入页应该被看成一个动态窗口系统,而不是一张固定尺寸的设计稿。


二、先建立三个边界概念

1. 窗口边界

窗口边界是应用当前能够绘制的完整区域。

开启沉浸式后,背景通常可以延伸到系统栏区域。

2. 安全内容边界

安全内容边界是关键交互元素不应越过的区域。

背景可以进入系统栏,但标题、返回按钮、发送按钮不应被遮挡。

3. 键盘可视边界

软键盘出现后,可视区域会变化。

输入框、候选操作和当前消息需要根据新的边界重新布局。

这三个边界必须分开理解:背景铺满不等于内容也要铺满。


三、推荐的页面分层

一个稳定的聊天或表单页面可以拆成四层:

  1. 全屏背景层;
  2. 安全区内容层;
  3. 底部输入与扩展面板层;
  4. 临时遮罩和弹层。

示意结构如下:

Stack() {
  this.buildBackground()
  Column() {
    this.buildHeader()
    this.buildContent()
    this.buildComposer()
  }
  this.buildOverlay()
}

背景负责视觉连续性,内容负责可用性,输入层负责跟随键盘,遮罩负责临时交互。


四、开启沉浸式后要做什么

沉浸式配置应集中在窗口创建或页面统一入口中处理。

不要在多个业务页面中反复设置窗口属性,否则页面切换时容易出现闪动和状态不一致。

推荐原则:

  • 窗口级能力在 Ability 或窗口管理层统一设置;
  • 页面只消费安全区结果;
  • 组件不直接猜测状态栏高度;
  • 不使用固定的顶部 24vp 或底部 34vp 作为通用答案;
  • 系统栏图标颜色要随背景明暗调整。

五、安全区数据应该如何管理

建议把安全区转换为明确的页面状态。

interface SafeAreaInsets {
  top: number
  bottom: number
  left: number
  right: number
}

页面状态示例:

@State private topInset: number = 0
@State private bottomInset: number = 0
@State private keyboardHeight: number = 0

实际项目应以当前 SDK 提供的窗口与避让区域 API 为准,避免复制旧版本接口名称。

安全区变化后,应更新状态,让布局直接引用状态值。

Column() {
  this.buildHeader()
  this.buildMessageList()
  this.buildComposer()
}
.padding({
  top: this.topInset,
  bottom: this.keyboardHeight > 0 ? 0 : this.bottomInset
})

这里的关键不是某个固定公式,而是保持状态来源单一。


六、为什么不能把安全区写死

写死安全区通常会在开发机上看起来正常,但会在以下环境失效:

  • 系统栏显示模式变化;
  • 手势导航与三键导航切换;
  • 平板任务栏出现;
  • 应用进入分屏或自由窗口;
  • 横屏后左右挖孔区域变化;
  • 字体放大导致标题栏内容增高;
  • 键盘使用悬浮模式;
  • 外接物理键盘连接或断开。

安全区是运行时数据,不是设计稿常量。


七、软键盘避让的两种基本策略

1. 调整窗口尺寸

键盘弹出时,应用可用区域缩小,页面重新布局。

适合:

  • 聊天页;
  • 长表单;
  • 输入框必须紧贴键盘的页面;
  • 内容区可以滚动的页面。

优点是输入区自然上移,缺点是复杂布局可能发生频繁测量。

2. 平移或覆盖布局

窗口尺寸不变,页面自行根据键盘高度移动输入区。

适合:

  • 自定义编辑器;
  • 画布类页面;
  • 必须保持主体尺寸的场景;
  • 对动画时序有精细控制的页面。

这种方式更灵活,但必须自行处理遮挡、滚动和焦点定位。


八、聊天页的稳定布局公式

聊天页可以把可用高度理解为:

消息区高度 = 窗口高度 - 顶部安全区 - 标题栏 - 输入区 - 当前底部避让

当前底部避让不能简单写成“键盘高度 + 底部安全区”。

更稳妥的规则是:

  • 键盘关闭:使用底部安全区;
  • 键盘打开并由系统缩放窗口:不重复叠加键盘高度;
  • 键盘覆盖窗口:使用实际键盘遮挡高度;
  • 扩展面板打开:使用扩展面板高度;
  • 键盘与扩展面板互斥:只保留当前活动面板高度。

重复计算是输入区被顶得过高的常见原因。


九、输入框获得焦点后的滚动策略

键盘出现后,仅让输入框可见还不够。

聊天页面通常还需要:

  • 保持最新消息可见;
  • 用户正在查看历史消息时不要强制跳到底部;
  • 新消息到达时判断是否展示“有新消息”提示;
  • 键盘动画结束后再执行一次精确滚动;
  • 列表数据变化与键盘变化不要形成循环更新。

可维护的判断条件包括:

@State private isNearBottom: boolean = true
@State private keyboardVisible: boolean = false
@State private unreadCount: number = 0

只有用户原本接近底部时,才自动滚动到最新消息。


十、键盘与表情面板必须互斥

聊天页常见四种底部状态:

  • 全部关闭;
  • 系统键盘打开;
  • 表情面板打开;
  • 附件面板打开。

不要用多个互不关联的布尔值随意组合。

推荐使用单一状态枚举:

enum ComposerPanel {
  NONE,
  KEYBOARD,
  EMOJI,
  ATTACHMENT
}

状态切换规则:

  • 点击输入框:切换到键盘;
  • 点击表情:先关闭键盘,再打开表情;
  • 点击附件:关闭其他面板,再打开附件;
  • 点击消息区:全部关闭;
  • 返回键:优先关闭当前面板,再处理页面返回。

单一状态源能避免键盘和面板同时占位。


十一、动画时序为什么重要

键盘是系统动画,自定义面板是应用动画。

如果二者同时开始且高度不同,输入区会明显跳动。

建议:

  1. 记录当前键盘可见状态;
  2. 请求隐藏键盘;
  3. 等待避让区域回落或焦点状态更新;
  4. 再显示自定义面板;
  5. 面板高度尽量与常用键盘高度接近;
  6. 切换过程中保持输入区锚点稳定。

不要依赖一个固定延时在所有设备上解决问题。


十二、表单页面如何避让

表单页与聊天页不同,重点是当前焦点字段必须可见。

推荐结构:

Column() {
  this.buildTitle()
  Scroll() {
    Column() {
      this.buildFormFields()
      this.buildSubmitButton()
    }
  }
}

表单避让要点:

  • 内容区域可滚动;
  • 输入项之间保留足够间距;
  • 最后一个输入框下方保留提交按钮空间;
  • 错误提示出现后重新判断可见性;
  • 密码管理器、验证码栏出现时也要验证;
  • 横屏下不要让键盘覆盖整张表单。

十三、弹窗内输入的特殊风险

弹窗通常位于 Stack 顶层,键盘出现后可能发生:

  • 弹窗整体被顶出顶部;
  • 标题可见但确认按钮不可见;
  • 遮罩拦截滚动;
  • 焦点切换导致弹窗反复测量;
  • 关闭弹窗后键盘仍残留;
  • 键盘关闭动画与弹窗退出动画冲突。

解决方向:

  • 弹窗最大高度基于当前可视区域;
  • 内容过长时让弹窗内部滚动;
  • 操作按钮固定在弹窗底部;
  • 关闭前主动释放焦点;
  • 不把弹窗高度写成屏幕固定百分比;
  • 小屏横屏场景优先改为全屏编辑页。

十四、状态响应式更新的注意点

ArkUI 中,布局应直接引用安全区和键盘状态。

不要在调用 Builder 时传入已经计算好的固定字符串或固定高度,再期待状态变化自动刷新。

推荐:

@Builder
buildComposer() {
  Row() {
    TextInput({ text: this.draft })
    Button('发送')
  }
  .padding({ bottom: this.keyboardHeight > 0 ? 0 : this.bottomInset })
}

状态变化时,真正依赖状态的 UI 节点才能稳定更新。


十五、横屏和多窗口不能最后再补

横屏下键盘高度占比更大,顶部和底部空间更紧张。

至少验证:

  • 手机竖屏;
  • 手机横屏;
  • 平板全屏;
  • 平板分屏;
  • 2in1 自由窗口;
  • 外接物理键盘;
  • 悬浮键盘;
  • 大字体模式。

多窗口适配的核心是根据当前窗口尺寸布局,而不是根据设备型号分支。


十六、常见错误与修复方向

错误 1:顶部和底部都写固定 padding

后果:不同设备遮挡或留白过大。

修复:读取运行时避让区域。

错误 2:系统已缩放窗口,又手动加键盘高度

后果:输入区被重复顶起。

修复:先确认窗口避让模式,再决定是否消费键盘高度。

错误 3:键盘、表情和附件用三个布尔值

后果:出现多个面板同时为真。

修复:使用互斥枚举状态。

错误 4:每次键盘变化都强制滚动到底部

后果:用户阅读历史消息时位置被打断。

修复:仅在接近底部时自动跟随。

错误 5:只在真机竖屏验证

后果:横屏、分屏和大字体下集中暴露问题。

修复:建立覆盖窗口形态的测试矩阵。


十七、调试方法

调试沉浸式问题时,建议临时显示以下数据:

  • 当前窗口宽高;
  • 顶部安全区;
  • 底部安全区;
  • 键盘遮挡高度;
  • 当前焦点字段;
  • 当前面板状态;
  • 消息列表是否接近底部;
  • 输入区最终 Y 坐标。

调试日志不要输出用户输入内容,避免泄露聊天文本、手机号或密码。


十八、自动化与人工测试如何分工

自动化测试适合验证:

  • 面板状态机;
  • 高度计算函数;
  • 键盘开关后的状态结果;
  • 返回键关闭顺序;
  • 空值和边界值。

人工测试更适合验证:

  • 系统键盘真实动画;
  • 输入区是否跳动;
  • 手势返回体验;
  • 多种输入法;
  • 悬浮键盘;
  • 无障碍字体;
  • 系统栏图标可读性。

两者结合才能覆盖体验和逻辑。


十九、发布前检查清单

1. 顶部安全区

  • 检查目标:标题、返回按钮和操作按钮未进入状态栏不可点击区域。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    2. 底部安全区

  • 检查目标:键盘关闭时输入区与手势区域保持安全距离。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    3. 背景延伸

  • 检查目标:背景可铺满系统栏区域但关键内容仍受约束。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    4. 系统栏颜色

  • 检查目标:浅色与深色背景下图标均清晰可读。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    5. 键盘打开

  • 检查目标:输入框和发送按钮完整可见。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    6. 键盘关闭

  • 检查目标:页面没有残留额外底部空白。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    7. 重复避让

  • 检查目标:系统缩放与手动高度没有重复计算。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    8. 表情切换

  • 检查目标:键盘与表情面板切换时输入区不跳动。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    9. 附件切换

  • 检查目标:附件面板不会与键盘同时占位。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    10. 返回键顺序

  • 检查目标:优先关闭面板,再关闭页面。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    11. 消息滚动

  • 检查目标:接近底部时才自动跟随最新消息。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    12. 历史阅读

  • 检查目标:键盘变化不会打断历史消息阅读位置。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    13. 草稿状态

  • 检查目标:切换面板不会清空未发送文本。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    14. 焦点恢复

  • 检查目标:关闭扩展面板后可正常恢复输入焦点。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    15. 空消息列表

  • 检查目标:没有消息时布局仍正确。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    16. 超长消息

  • 检查目标:长文本换行后不会挤压输入区。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    17. 多行输入

  • 检查目标:输入框达到最大高度后内部可滚动。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    18. 大字体

  • 检查目标:标题、输入框和按钮不会重叠。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    19. 横屏布局

  • 检查目标:键盘占用较大高度时仍可完成输入。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    20. 平板全屏

  • 检查目标:宽屏下输入区宽度与内容密度合理。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    21. 平板分屏

  • 检查目标:窄窗口下不依赖设备固定宽度。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    22. 自由窗口

  • 检查目标:拖动窗口尺寸后安全区及时更新。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    23. 物理键盘

  • 检查目标:连接键盘后不会保留软键盘占位。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    24. 悬浮键盘

  • 检查目标:窗口没有错误增加整块底部留白。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    25. 输入法切换

  • 检查目标:不同输入法高度变化可被正确处理。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    26. 候选栏变化

  • 检查目标:候选栏展开收起时页面不抖动。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    27. 弹窗输入

  • 检查目标:确认与取消按钮始终可见。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    28. 弹窗关闭

  • 检查目标:关闭后焦点和键盘状态被正确清理。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    29. 页面切换

  • 检查目标:返回再进入时不复用过期的避让高度。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    30. 前后台切换

  • 检查目标:恢复前台后重新同步窗口状态。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    31. 锁屏恢复

  • 检查目标:解锁后输入区位置保持正确。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    32. 主题切换

  • 检查目标:系统栏和背景颜色同步更新。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    33. 无障碍

  • 检查目标:读屏焦点顺序不受视觉层级干扰。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    34. 触控区域

  • 检查目标:底部按钮没有落入系统手势冲突区。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    35. 防误触

  • 检查目标:遮罩关闭逻辑不会拦截输入框操作。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    36. 性能

  • 检查目标:键盘动画期间没有高频数据库或网络操作。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    37. 列表性能

  • 检查目标:布局变化不触发消息列表无限重建。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    38. 状态单一

  • 检查目标:底部面板由一个明确状态源控制。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    39. 异常恢复

  • 检查目标:键盘事件缺失时页面仍能回到可用状态。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。

    40. 隐私日志

  • 检查目标:调试信息不包含输入内容和用户身份数据。

  • 操作方法:在至少两种窗口尺寸下触发该场景并观察布局变化。

  • 通过标准:核心内容无遮挡、无跳动、无重复留白且交互可以恢复。


二十、推荐的排查顺序

遇到输入区位置异常时,按以下顺序排查:

  1. 确认窗口是否开启沉浸式;
  2. 确认系统采用哪种键盘避让模式;
  3. 记录键盘前后的窗口高度;
  4. 检查是否同时消费了窗口缩放和键盘高度;
  5. 检查底部安全区是否重复叠加;
  6. 检查自定义面板是否残留占位;
  7. 检查焦点是否已经释放;
  8. 检查列表滚动是否反向影响布局;
  9. 在横屏和分屏中复现;
  10. 最后再调整动画参数。

先验证数据和状态,再调整视觉动画,能显著降低无效试错。


二十一、工程化落地建议

大型项目可以把能力拆分为:

  • 窗口沉浸式配置服务;
  • 安全区状态提供者;
  • 键盘可见性与高度监听器;
  • 输入面板互斥状态机;
  • 列表滚动协调器;
  • 多窗口测试用例;
  • 隐私安全日志规范。

页面负责组合这些能力,不应该自行复制整套窗口逻辑。

同时要注意监听器生命周期:页面消失或窗口销毁时及时解除订阅,避免重复回调与内存泄漏。


二十二、总结

沉浸式体验的本质是边界管理。

真正稳定的实现需要同时满足:

  • 背景充分利用屏幕;
  • 关键内容尊重安全区;
  • 键盘出现后输入操作不中断;
  • 系统键盘与自定义面板互斥;
  • 列表滚动尊重用户当前位置;
  • 横屏、分屏和外接键盘均可恢复;
  • 监听、状态与日志符合生命周期和隐私要求。

如果只记住一句话,可以记住:

沉浸式负责把视觉延伸到边缘,安全区负责把交互留在可用范围,键盘避让负责让输入过程始终连续。

img

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐