
去年接手一个模拟项目X需求很简单——聊天页面底部一个输入框点一下唤起安卓软键盘输入框要跟着键盘向上移动。iOS端没费什么劲就调好了换到安卓测试机输入框直接被键盘盖住。网上搜 uni.onKeyboardHeightChange相关帖子不少但真正把“打开键盘输入框向上移动”从头到尾讲透的没几篇多数只是贴一段代码评论区全在问“为什么我的没反应”“为什么键盘收回去又弹出来”。这篇文章就把我踩过的坑、试过的布局方案、以及最终沉淀下来的封装方式整理出来给同样被这个需求折磨过的朋友做个参考尤其是正在做聊天、评论、表单这类页面的应该能直接消化。需要先说明的是这个API主要解决的是 uni-app 在安卓 App 端的键盘适配问题。iOS 端不用管系统自己会处理H5 端也不需要管浏览器天生把输入框顶在键盘上方。所以后面所有内容默认都是针对安卓 App 端来聊的。1. 安卓键盘和iOS键盘行为逻辑天生不一样1.1 一个让我半夜改代码的需求我做那个聊天项目的时候一开始真的很天真。页面结构也简单上面一个消息列表下面一个固定底部的输入框iOS上测试聚焦、弹键盘、输入框自动上移一气呵成。结果打包到安卓真机完了输入框纹丝不动直接被键盘下半部分盖住用户打几个字全靠盲打。我当时第一反应是“是不是 CSS 定位的问题”于是把输入框从 fixed 改成 absolute又改成 flex 布局折腾了一晚上毫无起色。后来才明白这根本不是布局方式的问题而是安卓系统在键盘弹出时根本不保证你的页面容器会跟着调整。你用的 position 也好、flex 也好都是相对于当前 WebView 渲染窗口来定位的而 WebView 的窗口在键盘弹出后很多时候压根没有被系统重新计算。页面还是那个全屏页面输入框自然就停留在原来的位置被键盘当作“背景”盖掉。1.2 iOS不用管、安卓必须管的根源两个系统对软键盘的处理模型完全不同。iOS 的软键盘是悬浮在应用窗口之上的弹出时系统会主动把当前页面的安全区域上边界抬升底部固定元素天然跑到键盘上方几乎不需要开发者介入。这一点你用原生开发、用 Flutter、用 uni-app体验都差不多。安卓则复杂得多。安卓的窗口系统会按照配置决定键盘弹出后的行为常见两种模式adjustPan 会把整个窗口往上平移让聚焦的输入框尽量可见adjustResize 是直接压缩可绘制区域的尺寸。但问题在于uni-app 的 App 端页面跑在 WebView 里WebView 内部 HTML 布局是否跟着系统 resize 一起重新计算取决于很多因素。实测中大部分页面的根容器高度仍然保持整个屏幕的高度键盘把屏幕底部盖住之后你的输入框并不会自动逃离键盘区域。这就是 uni.onKeyboardHeightChange 存在的原因让 JS 层能够拿到真实的键盘高度再由开发者根据这个高度去调整布局。它不是万能的但没有它App 端做键盘适配连入口都找不到。1.3 这个API的适用范围先对齐我在项目里第一次接这个 API 的时候看到它名字里有“KeyboardHeightChange”以为所有平台都可以统一用。后来查文档才发现这个 API 实际上主要面向 App-Android 平台。在 iOS 上强行用的话虽然不至于报错但可能引发无意义的布局变化因为 iOS 原生已经帮你处理好了。在 H5 上更没必要浏览器键入框会自动浮在键盘上。所以我的第一个建议是在写任何键盘适配代码之前先用条件编译把平台限定住。后面我在封装 mixin 时也用了这套思路避免同一套逻辑在不同的端上帮倒忙。2. 先把uni.onKeyboardHeightChange这个API吃透2.1 基本用法和回调参数这个 API 的用法极简uni.onKeyboardHeightChange((res) { console.log(键盘高度变化, res.height); });回调参数里只有一个 height单位是逻辑像素 px。height 大于 0 说明键盘处于弹起状态等于 0 说明键盘已收起。听起来是不是很简单但就是这么一个简单的 API用不好照样翻车。首先这个 height 是逻辑像素不是 rpx也不是物理像素。如果你要把这个值直接绑定到样式里比如给底部输入框设置 bottom 值那就直接绑定不用做任何换算。如果你非要先换算成 rpx那就得用px / 屏幕宽度 * 750这种公式增加了计算量还可能因为屏幕宽度不同产生误差。没必要底部的布局偏移用 px 是最自然的。其次键盘弹起和收起的回调时机不完全对称。键盘弹起时回调会跟随动画连续触发多次键盘收起时理论上最后会有一个 height 为 0 的通知但有些定制 ROM 上这个“归零”通知可能迟到甚至不触发。这一点我后面会详细讲。2.2 别把回调当成“弹起瞬间通知”这个 API 最容易让人误解的地方就是大家总以为它是在键盘弹起那一刻通知你一声然后你去执行一些一次性动作比如“滚动到底部”“把发送按钮亮起来”。实际上它更像是一个持续更新的“键盘高度状态流”。在安卓上软键盘弹出是一个渐进的动画过程回调值可能是 0、80、150、240、300 这样一路涨上来的。如果你的回调里做的是频繁且昂贵的操作比如每次 height 一变就重新计算列表高度、重新渲染整个页面那用户会明显感觉到卡顿甚至在键盘还没弹完的时候页面就已经抖了好几轮。正确的思路是这个 API 负责把“当前键盘高度”持续告诉你你的职责是把高度反映到布局上。至于“弹起之后我要额外做什么”不要在回调里直接做而是等 height 稳定下来后再做一次。比如滚动到底部我会在判断 height 大于某个阈值之后用 this.$nextTick 加一个短暂延时再滚这样键盘动画已经进行了一大半滚动的视觉错位感会小很多。2.3 生命周期管理回调叠加是最隐蔽的bug这里必须重点说一个坑也是我一度被坑得最惨的地方。最初我把监听写在输入框的 focus 事件里想着“聚焦时监听失焦时移除”逻辑上听起来更精准。结果用户每次点击输入框focus 触发一次uni.onKeyboardHeightChange 就注册一次回调。连续点几次之后同一个键盘高度变化会同时触发多个回调页面里的键盘高度变量被反复赋值输入框直接在键盘上方抖来抖去像得了帕金森一样。正确的做法是把回调注册放在页面的 onLoad 或 onShow 里只注册一次在 onUnload 里移除。而且 off 的时候必须传入同一个函数引用。如果你用箭头函数写匿名回调off 的时候再写一个一模一样的箭头函数那其实是两个完全不同的引用根本移除不掉。我后来干脆把回调函数在 onLoad 里先赋值给 this注册和移除都引用同一个。export default { onLoad() { // 先移除再注册双保险 this.handleKbHeight (res) { // 具体的键盘高度处理逻辑 }; uni.offKeyboardHeightChange(this.handleKbHeight); uni.onKeyboardHeightChange(this.handleKbHeight); }, onUnload() { uni.offKeyboardHeightChange(this.handleKbHeight); } };提示uni.offKeyboardHeightChange 是可以不传参数的不传会移除所有键盘监听。但为了精确管理强烈建议传入同一个函数引用避免页面销毁后旧回调还在触发。3. 输入框随键盘上移的两种主流量产做法3.1 方案一底部固定条 动态bottom聊天样式的首选最直接、也是最适合聊天页面的做法就是把输入框所在的容器用 position: fixed 固定在屏幕底部然后用监听得到的键盘高度动态修改它的 bottom 值。模板层大概是这样的view classchat-page scroll-view classmsg-list :scroll-into-viewscrollIntoView :scroll-ytrue view :idmsg- index classmsg-item v-for(item, index) in msgList :keyindex text{{ item.content }}/text /view /scroll-view view classinput-bar :style{ bottom: keyboardHeight px } input classinput-box v-modeldraft placeholder请输入消息 confirm-typesend / button classsend-btn sizemini clicksendMsg发送/button /view /view关键样式和逻辑.input-bar { position: fixed; left: 0; right: 0; bottom: 0; display: flex; align-items: center; padding: 12rpx 20rpx; background: #fff; z-index: 100; }uni.onKeyboardHeightChange((res) { const h res.height || 0; this.keyboardHeight h 30 ? h : 0; });一个很容易忽略的点动态绑定 bottom 时不要给 input-bar 加 transition 属性。很多人为了让布局看起来平滑给 bottom 加了 0.3s 的过渡动画。结果就是键盘已经弹到一半甚至完全弹起来了输入框还在慢悠悠地往上飘视觉上非常难受总感觉慢半拍。正确的做法是不要加过渡让输入框严格跟随键盘高度更新的节奏走。你想想真实的原生键盘弹出效果从来都是键盘和输入框同步动没有谁滞后。还有一个细节是底部背景问题。input-bar 被顶到键盘上方后它原来的位置会空出来键盘动画期间那个区域会短暂露出页面底部的背景色看起来像一块闪空的缺口。解决办法是给聊天页面最外层容器设置和 input-bar 一样的背景色或者把整块底部区域做成一个统一背景避免穿帮。这个问题很多人不注意等用户截图给你的时候才尴尬。3.2 方案二内容区动态paddingBottom表单页面的兜底做法但底部固定条并不是所有场景都适用。如果你做的是一个内容比较长的表单页面输入框在中间偏下位置还有一大段内容和它并列那更适合的方案是给页面根容器动态加 paddingBottom把整个页面的可用空间向下扩展。做法很简单页面根容器绑定 paddingBottomview classform-page :style{ paddingBottom: keyboardHeight px } !-- 表单内容 -- view classcomment-box textarea v-modelcontent placeholder写下你的评论 / /view /view这个方案的好处是不管输入框在哪个位置只要页面本身可以滚动键盘弹起时底部就有了额外的留白空间用户手动滚动也能让输入框露出来。配合 uni.pageScrollTo 把页面滚到合适位置体验可以做到接近原生。我在实测中发现方案二的关键在于“滚动时机”。键盘弹起后页面可用高度变小你得主动把输入框滚到视口内。可以先通过 uni.createSelectorQuery 取到输入框的位置再计算目标 scrollTop。代码大概是这样let query uni.createSelectorQuery(); query.select(.comment-box).boundingClientRect((rect) { let h this.keyboardHeight; let windowInfo uni.getWindowInfo(); // 把输入框滚到未被键盘遮挡的区域内 let targetY rect.top - (windowInfo.windowHeight - h - rect.height); if (targetY 0) { uni.pageScrollTo({ scrollTop: targetY, duration: 0 }); } }).exec();为什么 duration 要设成 0因为我希望滚动和键盘弹起完全同步不要出现“键盘还在飞、页面已经在滚”的错位感。真机上 0 毫秒就是瞬间到位用户看着键盘和页面一起动体感反而更跟手。3.3 两种方案的取舍思路我的经验是聊天、客服、评论回复这类“输入框永远在底部”的页面用方案一把输入条直接 fixed 在底部简单粗暴且效果好网上购物评价、后台表单这类“页面内容长、输入框位置不固定”的页面用方案二给页面加 paddingBottom再用滚动把输入框带出来。但不管哪种方案都不建议每个页面复制粘贴同一套监听代码。我最初在四五个页面里各写了一份 uni.onKeyboardHeightChange后来发现问题越来越多有的页面忘了在 onUnload 里移除有的页面把回调写成了箭头函数导致 off 失效有的页面居然在 tabBar 页面里也监听了键盘页面切换时旧监听还残留。把这些公共逻辑抽成 mixin 或独立工具模块是必然的封装内容放在第 5 节。4. 安卓机型适配实战状态栏、全面屏、动画回调这些“意外”4.1 键盘动画期间的渐增回调别乱加防抖安卓的软键盘弹出来不是一个瞬间动作而是从屏幕底部往上滑的动画时长通常在 200ms 到 400ms 之间。uni.onKeyboardHeightChange 在这段时间里会连续回调height 值从十几、几十一直涨到最终的键盘高度。很多开发者看到回调这么密集第一反应是“要不要防抖一下只取最终值”。我的答案是不要。防抖意味着你要舍弃中间的状态而键盘动画期间输入框不跟着键盘动用户看到的画面就是“键盘一直在涨输入框原地不动最后再跳一下”。这同样很掉价。正确做法是让布局完全跟随这个渐增的高度值输入框和键盘一起向上移动就像被键盘顶上去一样。如果项目里因为某些原因必须做节流也把间隔控制在 50ms 以内不要狠狠截断。真正该做防抖的地方只有一个键盘收起时。因为部分 ROM 在键盘收起过程中会先回传一个很小的 height 值比如十几二十像素然后又跳回 0甚至有些输入法在“收起动画”中会短暂触发一次 height 为 0然后再次触发一个小高度又归零。我的做法是统一做阈值判断只有 height 大于 30px 才认为是弹起否则一律当成 0。这个 30px 经过好几台机器验证低一点高一点影响不大但能过滤掉大部分“假收起”和“假弹起”。4.2 状态栏高度、导航栏和底部安全区带来的换算误差不少开发者在拿到 res.height 之后直接拿它当布局的 bottom 值大多数机型是没问题的。但在全面屏、带虚拟按键、带手势条的机型上偶尔会出现输入框被顶得偏高或者偏低。我的排查经验是问题通常不在这个 API 本身而在页面布局有没有额外的偏移。如果你的页面是沉浸式状态栏整块屏幕从上到下都是可绘制区域那么 res.height 直接等于需要上移或垫高的距离没有多余换算。但如果你的页面外面还套了一层原生的导航栏或者输入条本身又做了 safe-area-inset-bottom 的计算这两部分就容易和 res.height 重复计算导致输入框高出键盘一截。最稳妥的做法是先用一个基础版本跑真机观察输入框底部边缘和键盘顶部边缘是否贴合。如果不贴合再考虑通过 uni.getSystemInfoSync 获取 statusBarHeight 或 safeAreaInsets.bottom 做加减修正。不要一上来就盲改公式因为不同 ROM 的返回值差异很小多数情况下默认值就是对的。至于全面屏底部的横条手势条我在真机上验证过uni.onKeyboardHeightChange 返回的 height 实测已经包含了手势条区域直接用它设置 bottom输入框和键盘贴得刚刚好。如果我额外再补一个 env(safe-area-inset-bottom)反而会多出一截输入框看得人以为它悬空了。这条经验的本质是在提醒我手势条的安全区这个 API 已经替你考虑了不要再画蛇添足。4.3 输入法厂商的“隐藏模式”悬浮键盘和手写切换安卓上的输入法有好几种形态最常见的坑来自“悬浮键盘”。部分品牌输入法允许用户把键盘设置成悬浮在屏幕中间偏下的位置此时系统上报的回调里 height 值会变得不稳定有时变成 0有时给一个奇怪的完整键盘高度。在悬浮键盘模式下靠 res.height 去做底部上移基本会失效。我的处理原则是这个场景属于用户主动修改输入法形态应用层很难做到完美适配只要保证“键盘不遮住输入框”这条底线不破就行。所以当 height 突然从大于 0 跳到 0但输入框依然处于焦点状态时不要立刻把所有布局恢复原状而是等几百毫秒如果输入法真的是收起了再归零。这样在悬浮键盘模式下至少不会出现输入框反复跳动。另一个常见情况是键盘在手写、拼音、笔画之间切换时height 在切换瞬间会跳变一下比如从 300px 跳到 200px 再回到 300px。这种跳变是真实的键盘高度变化底部布局理应跟着调整不需要特殊处理。真正需要留意的是不要在切换过程中去滚动页面否则用户视角里页面会自己跳来跳去。我一般在 height 变化稳定 100ms 之后才触发 scrollIntoView 之类的滚动动作这样用户不会看到页面瞬移。4.4 manifest里的softinputMode调错方向会双重位移这个配置最容易被忽略一旦错了后果也最严重。uni-app 在 App 端有一个 softinputMode 配置位于 manifest.json 的 app-plus 节点下默认值是 adjustResize也可以改成 adjustPan。{ app-plus: { softinputMode: adjustResize } }这两者的区别前面讲过adjustPan 是系统把整个窗口往上平移让输入框露出来adjustResize 是压缩窗口高度让页面容器重新计算。在 uni-app 的多数页面里建议保持默认的 adjustResize然后完全交给 uni.onKeyboardHeightChange 来做上移。如果你把 softinputMode 改成 adjustPan同时又在回调里手动把输入条往上推两个机制会叠加输入框会被推得比键盘顶部还高一截这就是标准的“双重位移”。反过来如果遇到“键盘弹起来时输入框已经被系统顶起来了但我的监听又加了一次 height”的情况第一件事就是去确认 manifest 里的 softinputMode 是不是被改成了别的值。我认识的一位开发者就是页面怎么调都不对折腾了大半天最后发现是手动改了配置文件加了 adjustPan。这属于配置上的低级错误但确实非常隐蔽。注意softinputMode 是全局配置一旦设置会作用于所有 App 页面。如果你在大部分页面都要自己接管键盘位移就保持 adjustResize如果个别特殊页面需要系统默认行为也别为了它去改全局配置宁可那个页面多写一点自定义逻辑。5. 从踩坑到稳定测试验证清单与通用封装5.1 一个能让你少被用户骂的实测清单键盘适配做完之后建议至少按下面清单过一遍很多问题是开发环境暴露不出来的真机优先模拟器为辅。模拟器上的键盘行为与真实 ROM 差异很大尤其悬浮键盘和安全区模拟器基本测不出来。至少覆盖三类机型一台带虚拟三键的老机型、一台全面屏带手势条的机型、一台刘海屏机型。键盘高度、安全区、状态栏的差异集中在这三类机器上。切换输入法。如果设备装了多个输入法每个都试一遍重点试有“悬浮键盘”模式的输入法。在键盘弹起状态下点击发送或提交观察输入框位置在数据刷新后是否还正常。从包含输入框的页面返回上一页或切 tab确认页面销毁后有没有残留的键盘监听导致报错。我一般会在每台测试机上开一个简单的测试页面把 onKeyboardHeightChange 回调的 height 值实时显示在屏幕上然后手动弹起、收起键盘记录最大值、最小值、归零时机。数据一对比哪些机器回调稳定、哪些机器会多回几次 0心里就完全有数了。下面这张表是我自己积累下来的问题现象、原因以及处理方向的汇总先给出来方便你排查时对照现象可能原因处理方向输入框被键盘完全盖住布局未跟随键盘高度使用 uni.onKeyboardHeightChange 动态偏移输入框高于键盘一截输入条自带安全区且与回调高度叠加去掉 safe-area-inset-bottom 或适当减高键盘收回后输入框不回位收起回调未触发或阈值过滤失效增加归零判断与延时兜底连续点击输入框后页面抖动焦点事件里重复注册监听改到 onLoad/onShow 注册onUnload 移除输入框被顶到屏幕外softinputMode 配置与手动偏移叠加保持 adjustResize取消手动平移逻辑悬浮键盘模式下高度不稳输入法自定义形态系统上报异常延迟归零、阈值过滤保证输入框不被遮5.2 我沉淀下来的通用封装mixin 页面钩子最后把这套经验收敛成一个公共 mixin后续新页面直接引入就能用。它的职责很纯粹管理好键盘监听的生命周期把 height 值经过阈值清洗后暴露给页面再提供一个可选的回调钩子让页面自己决定怎么响应。// mixins/with-keyboard.js export default { data() { return { keyboardHeight: 0, keyboardShow: false, __kbHeightTimer: null }; }, onLoad() { // #ifdef APP-PLUS this.__kbHandler (res) { const raw res.height || 0; if (this.__kbHeightTimer) { clearTimeout(this.__kbHeightTimer); } this.__kbHeightTimer setTimeout(() { const h raw 30 ? raw : 0; this.keyboardHeight h; this.keyboardShow h 0; if (typeof this.onKeyboardHeightChange function) { this.onKeyboardHeightChange(h, this.keyboardShow); } }, 30); }; uni.offKeyboardHeightChange(this.__kbHandler); uni.onKeyboardHeightChange(this.__kbHandler); // #endif }, onUnload() { // #ifdef APP-PLUS if (this.__kbHeightTimer) { clearTimeout(this.__kbHeightTimer); this.__kbHeightTimer null; } uni.offKeyboardHeightChange(this.__kbHandler); // #endif } };这里加了一个 30ms 的轻度节流不是为了防抖主要是降低高频回调对页面渲染造成的压力。30ms 的延迟用户完全感知不到但渲染频率会降低不少动画也不会因此卡顿。页面里这样使用import withKeyboard from /mixins/with-keyboard.js; export default { mixins: [withKeyboard], methods: { // 覆盖 mixin 里定义的可选钩子 onKeyboardHeightChange(height, isShow) { // 页面自己决定怎么响应 } } };封装完这套以后我再也没有在业务页面里直接写过 uni.onKeyboardHeightChange。可以说90% 的页面只需要关心 onKeyboardHeightChange 这个方法里传来的 height 值该怎么用剩下的监听注册、清理、异常过滤mixin 都替我接管了。最后再分享一点真实体会键盘适配这件事本身没有什么高深的技术含量难的是它横跨了系统机制、布局方案、机型差异三个层面每个层面都有若干不起眼的坑。把方案确定下来并封装成公共能力之后后面的业务开发几乎无感但这套能力的价值恰恰体现在你不需要再为每个新页面重新踩一遍我踩过的坑。如果你正被安卓键盘折磨不妨先按我给的思路搭一版最简方案再逐步加机型适配应该会比你一上来就搜各种碎片代码要顺得多。