请添加图片描述

前言

在移动端开发中,「输入框被软键盘挡住」是最常见、也最令人沮丧的体验问题之一。用户点击页面底部的备注框,键盘弹起,光标和输入区域消失在键盘下方——用户看不到自己在打什么字,只能凭感觉盲打,或者反复拖动页面碰运气。

不少 HarmonyOS 开发者初次遇到这个问题时的反应是:

「我明明用了 Scroll,为什么还是挡?」
「加了 paddingBottom: 300 就好了,但不同手机键盘高度不一样……」
「听说有个 avoidArea,到底怎么用?」

HarmonyOS 提供了两套互补机制来解决键盘遮挡:

  1. 系统级避让UIContext.setKeyboardAvoidMode(),由框架自动顶起或压缩页面;
  2. 窗口级避让区(avoidArea):通过 window.getWindowAvoidArea()avoidAreaChangekeyboardHeightChange 获取键盘占用区域,开发者手动调整 padding / 滚动位置。

本文结合项目中的可运行 Demo,从原理、API 对比、代码走读、手动 padding 实战到选型建议,做一次完整解析。读完后,你应该能回答:

  • OFFSETRESIZE 有什么区别?
  • avoidAreakeyboardHeightChange 该听哪个?
  • 什么时候用系统避让,什么时候自己算 padding?

一、问题本质:键盘占用的是「屏幕底部矩形区域」

软键盘弹出时,系统会在屏幕底部划出一一块不可布局区域——即 Avoid Area(避让区)。窗口的可交互内容区被压缩,若不做处理,原本位于底部的 TextInput / TextArea 就会与键盘重叠。

┌─────────────────────────────┐
│  状态栏 / 标题               │
├─────────────────────────────┤
│                             │
│   Scroll 内容区              │
│   ...                        │
│   [ 备注输入框 ]  ← 聚焦     │  ← 若在此,易被遮挡
├─────────────────────────────┤
│   ███ 软键盘 ███             │  ← AvoidArea TYPE_KEYBOARD
└─────────────────────────────┘

解决思路只有两类:

思路 谁来做 典型 API
系统自动避让 框架 KeyboardAvoidMode.OFFSET / RESIZE
开发者手动补偿 业务 keyboardHeightChangepaddingBottom

二、KeyboardAvoidMode:系统避让模式全解析

2.1 API 入口

在任意 @Component 内,通过 UIContext 设置:

import { KeyboardAvoidMode } from '@kit.ArkUI'

// 在 aboutToAppear 或页面进入时
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)

注意KeyboardAvoidMode 需从 @kit.ArkUI(即 @ohos.arkui.UIContext)导入,其枚举值为 OFFSETRESIZEOFFSET_WITH_CARETRESIZE_WITH_CARETNONE 等。不要与 Dialog 里另一套 KeyboardAvoidMode 混淆。

2.2 各模式对比表

模式 行为 适用场景 Demo 场景 id
OFFSET 整体上移页面,使聚焦输入框露出 普通表单、注册页 SYSTEM_OFFSET
RESIZE 压缩窗口可视高度,内容区变短 聊天页、IM 底部输入栏 SYSTEM_RESIZE
OFFSET_WITH_CARET 上移,且光标位置变化时也调整 多行文本、光标移动
RESIZE_WITH_CARET 压缩,且光标变化时也调整 复杂编辑器
NONE 不做任何避让 全屏游戏、自定义键盘 UI WRONG_NONE(踩坑演示)

默认模式OFFSET(API 11 文档说明)。页面退出时 Demo 会恢复为 OFFSET,避免影响其他页面:

aboutToDisappear(): void {
  this.avoidController.detach()
  this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)
}

2.3 OFFSET vs RESIZE 怎么选

  • OFFSET(整体上移):整个页面上移一截,顶部内容可能被推出屏幕。适合字段不多的表单,用户本来就会滚动。
  • RESIZE(压缩高度):页面「窗口」变矮,内部 Scroll 的滚动区域随之变短,底部输入栏相对键盘位置固定。适合「消息列表 + 底部输入框」的 IM 结构。

Demo 中切换场景 chip 即可在真机上对比两种模式的视觉差异。

2.4 踩坑模式:NONE

Demo 故意提供 WRONG_NONE 场景:

uiContext.setKeyboardAvoidMode(KeyboardAvoidMode.NONE)

此时系统完全不顶起页面。配合底部 TextArea(备注字段),键盘弹起后输入框几乎一定被遮挡。Demo 在检测到风险时显示警告:

private isBlockedRisk(): boolean {
  return this.selectedCase === KeyboardAvoidCase.WRONG_NONE &&
    this.keyboardHeightVp > 0 &&
    (this.focusedField === 'remark' || this.focusedField === 'address')
}

常见误触 NONE 的场景:从其他模块复制了 setKeyboardAvoidMode(NONE) 却忘记改回;或在全屏自定义页面设置了 NONE 后未在子页面恢复。


三、avoidArea 实战:窗口避让区 API

3.1 什么是 avoidArea

avoidArea 是窗口管理器维护的不可显示区域描述,类型由 AvoidAreaType 枚举:

AvoidAreaType 含义
TYPE_SYSTEM 系统栏(状态栏、导航栏)
TYPE_CUTOUT 刘海 / 挖孔
TYPE_KEYBOARD 软键盘
其他系统区域

与键盘遮挡直接相关的是 TYPE_KEYBOARD

3.2 三种获取键盘高度的方式

Demo 封装在 KeyboardAvoidController.ets 中,统一监听并上报来源:

方式 1:keyboardHeightChange 事件
win.on('keyboardHeightChange', (heightPx: number): void => {
  listener(uiContext.px2vp(heightPx), 'keyboardHeightChange')
})
  • 回调参数为 px
  • 键盘收起时高度为 0
  • 最直接、最常用。
方式 2:avoidAreaChange 事件
win.on('avoidAreaChange', (options: window.AvoidAreaOptions): void => {
  if (options.type === window.AvoidAreaType.TYPE_KEYBOARD) {
    listener(uiContext.px2vp(options.area.bottomRect.height), 'avoidAreaChange')
  }
})
  • 避让区变化时触发(键盘、系统栏等);
  • 需过滤 type === TYPE_KEYBOARD
  • 适合需要同时处理多种 avoidArea 的复杂页面。
方式 3:getWindowAvoidArea 同步读取
const initArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_KEYBOARD)
listener(uiContext.px2vp(initArea.bottomRect.height), 'getWindowAvoidArea')
  • 页面初始化时获取当前键盘区高度;
  • 适合进入页面时键盘已打开的边缘场景。

3.3 px 与 vp 转换

窗口 API 返回 px,布局使用 vp。必须通过 UIContext.px2vp() 转换:

const heightVp = uiContext.px2vp(heightPx)

Demo 顶部状态栏实时显示:

模式: OFFSET | 键盘高度: 312 vp | 来源: keyboardHeightChange

3.4 生命周期:注册与注销

aboutToAppear(): void {
  const hostContext = this.getUIContext().getHostContext()
  this.avoidController.attach(hostContext, this.getUIContext(), listener)
}

aboutToDisappear(): void {
  this.avoidController.detach()  // off 事件,防泄漏
}

务必在 aboutToDisappearoff 监听,否则页面销毁后仍收到回调,可能引发内存泄漏或已销毁组件的状态更新错误。


四、手动 padding:关闭系统避让 + 自己顶起

4.1 为什么需要手动方案

系统 OFFSET/RESIZE 在大多数页面够用,但以下场景可能需要手动控制:

  • 页面有多层嵌套 Scroll,系统顶起范围不符合预期;
  • 只需顶起底部某一块区域(如工具栏),而非整页;
  • 键盘上方需要固定显示自定义「完成」栏;
  • Navigation、半屏 Sheet 组合时行为需精细定制。

4.2 Demo 实现逻辑

选手动模式时:

  1. setKeyboardAvoidMode(KeyboardAvoidMode.NONE) —— 关闭系统避让,避免双重顶起
  2. 监听键盘高度,写入 @State manualPaddingVp
  3. ScrollColumn 设置动态 paddingBottom
.padding({
  left: 0,
  right: 0,
  top: 0,
  bottom: this.manualPaddingVp   // 随键盘高度变化
})

键盘收起时 keyboardHeightChange 回调 0manualPaddingVp 归零,布局恢复。

4.3 手动方案注意事项

要点 说明
必须配合 Scroll 否则 padding 只撑开容器,输入框仍可能不在可视区
键盘收起要归零 忘记恢复 padding 会留下大块空白
不要与 OFFSET 叠加 系统已顶起时再 padding 会「顶两次」
考虑安全区 底部有 Home 指示条时需额外处理

五、Demo 代码走读

5.1 场景配置(KeyboardAvoidTypes.ets)

四种场景一目了然:

export const KEYBOARD_AVOID_SCENARIOS: KeyboardAvoidScenario[] = [
  { id: WRONG_NONE,        title: '❌ NONE · 不避让键盘', ... },
  { id: SYSTEM_OFFSET,     title: '✅ OFFSET · 整体上移', ... },
  { id: SYSTEM_RESIZE,      title: '✅ RESIZE · 压缩布局', ... },
  { id: MANUAL_AVOID_AREA, title: '✅ avoidArea 手动 padding', ... },
]

5.2 模式切换(KeyboardAvoidPitfallDemo.ets)

private applyKeyboardAvoidMode(): void {
  const uiContext = this.getUIContext()
  if (this.selectedCase === WRONG_NONE || this.selectedCase === MANUAL_AVOID_AREA) {
    uiContext.setKeyboardAvoidMode(KeyboardAvoidMode.NONE)
  } else if (this.selectedCase === SYSTEM_OFFSET) {
    uiContext.setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)
  } else {
    uiContext.setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
  }
}

手动模式与 NONE 共用 KeyboardAvoidMode.NONE,区别在于手动模式额外应用 manualPaddingVp

5.3 表单设计(FeedbackFormSection.ets)

表单包含 5 个输入控件,「备注」TextArea 放在最底部,专门用于测试遮挡:

TextArea({ text: this.form.remark, placeholder: '点击这里测试键盘遮挡…' })
  .onFocus((): void => { this.focusedField = 'remark' })

在 NONE 模式下,该字段高亮红色边框(highlightBottom: true),提示用户这是测试焦点。


六、safeAreaPadding 与 expandSafeArea 补充

除窗口 avoidArea 外,ArkUI 组件级还提供:

// API 14+:组件安全区内边距
.safeAreaPadding({ bottom: 12 })

// 扩展组件布局到安全区外
.expandSafeArea([SafeAreaType.KEYBOARD], [SafeAreaEdge.BOTTOM])

SafeAreaType.KEYBOARD 表示软键盘区域。复杂页面可将 系统 KeyboardAvoidMode + 组件 safeAreaPadding 组合使用,而不是只靠一种手段。

Demo 要点文案中已收录:

复杂页可 safeAreaPadding 配合键盘区 SafeAreaType.KEYBOARD


七、常见错误与排查

7.1 错误清单

现象 可能原因 修复
底部输入框被挡 误设 NONE OFFSETRESIZE
页面被顶太高 OFFSET + 手动 padding 叠加 手动模式时设 NONE
padding 不消失 未监听高度变 0 检查 keyboardHeightChange
高度不对 直接用 px 当 vp px2vp() 转换
监听无效 未 getLastWindow window.getLastWindow(context)
泄漏 / 崩溃 未 off 监听 aboutToDisappear 中 detach
Scroll 无效 外层 Column 无高度约束 内层 Scroll 设 layoutWeight(1)

7.2 排查流程

输入框被挡?
  ├─ getKeyboardAvoidMode() 是否为 NONE → 改为 OFFSET/RESIZE
  ├─ 是否嵌套多层 Scroll → 简化结构或改 RESIZE
  ├─ 是否手动 padding 未生效 → 检查 keyboardHeightChange 绑定
  └─ 是否 px/vp 混用 → 统一 px2vp

八、方案选型决策树

需要键盘避让?
  │
  ├─ 普通表单页 ──────────────→ KeyboardAvoidMode.OFFSET(默认)
  │
  ├─ 聊天/列表+底栏 ─────────→ KeyboardAvoidMode.RESIZE
  │
  ├─ 多行编辑、光标频繁移动 ─→ OFFSET_WITH_CARET / RESIZE_WITH_CARET
  │
  ├─ 全屏自定义/游戏 ────────→ NONE + 自绘键盘
  │
  └─ 局部顶起/复杂嵌套 ──────→ NONE + avoidArea 手动 padding
         │
         ├─ keyboardHeightChange(首选)
         └─ avoidAreaChange TYPE_KEYBOARD(备选)

九、真机体验指南(配合 Demo)

  1. 运行 App,默认选中 ❌ NONE
  2. 滚动到底部,点击 「备注」 TextArea;
  3. 观察:键盘弹起,输入框被挡,顶部出现 ⚠️ 警告,状态栏显示键盘高度;
  4. 切换 ✅ OFFSET,再次聚焦备注 → 页面自动上顶,输入框可见;
  5. 切换 ✅ RESIZE → 可视区压缩,Scroll 滚动范围变短;
  6. 切换 ✅ 手动 avoidArea → 观察绿色提示 paddingBottom = N vp,Scroll 底部留出键盘等高空白。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

十、总结

软键盘遮挡输入框,不是 Scroll 的 bug,而是键盘避让策略未正确配置。

核心结论 说明
首选方案 大多数页面用 KeyboardAvoidMode.OFFSET
IM 场景 RESIZE 压缩可视高度
关闭避让 NONE 会导致底部输入框被挡
avoidArea TYPE_KEYBOARD 描述键盘占用矩形
监听 API keyboardHeightChange(px)+ px2vp
手动顶起 NONE + 动态 paddingBottom,键盘收起归零
生命周期 aboutToDisappear 注销监听、恢复默认模式

记住一句话:

简单页交给 OFFSET,聊天页用 RESIZE,精细控制听 avoidArea,用完记得 off 监听。


附录:Demo 文件结构

文件 职责
KeyboardAvoidTypes.ets 场景枚举、要点、表单模型
KeyboardAvoidController.ets 窗口监听封装
FeedbackFormSection.ets 多输入框测试表单
KeyboardAvoidPitfallDemo.ets 主交互页
pages/Index.ets 入口

附录:快速参考卡

// 系统避让
import { KeyboardAvoidMode } from '@kit.ArkUI'
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)

// 键盘高度监听
import { window } from '@kit.ArkUI'
const win = await window.getLastWindow(context)
win.on('keyboardHeightChange', (hPx) => {
  this.kbPadding = this.getUIContext().px2vp(hPx)
})
win.on('avoidAreaChange', (opt) => {
  if (opt.type === window.AvoidAreaType.TYPE_KEYBOARD) {
    this.kbPadding = this.getUIContext().px2vp(opt.area.bottomRect.height)
  }
})

// 手动补偿
Scroll() {
  Column().padding({ bottom: this.kbPadding }) { /* 表单 */ }
}

// 清理
win.off('keyboardHeightChange', handler)
this.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.OFFSET)
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐