uni-app小程序键盘弹起布局错乱:根因分析、修复方案与封装实践
做uni-app小程序表单页最怕的就是键盘一弹页面乱掉。我上一轮改资料填写页为了一个位置错乱问题把adjust-position开开关关试了二十多遍最后才想明白真正靠谱的是组合拳而不是某个单一属性。这篇文章就把键盘弹起导致布局错乱这个事从头到尾讲透为什么会乱、几套由浅入深的修复思路、微信和支付宝这些端各自什么脾气、以及一个我封装好直接能用的工具函数。不管你是正在改登录页、注册页、搜索页还是被订单填写页折磨到怀疑人生这篇都值得花十分钟看完。1. 键盘弹起时页面到底经历了什么1.1 根因webview可视区域被系统接管很多刚从H5转过来的同事会有一个误区键盘弹起不就相当于浏览器窗口变小了吗我用flex、百分比自适应不就行了事实完全不是这样。小程序页面本质是跑在webview里的H5页面。当软键盘弹起时微信客户端会直接改变webview的可视区域大小。这个改变的策略不同平台、不同机型、甚至不同输入法都不一样最典型的就是要么压缩webview高度要么整体上推页面。微信小程序给input组件内置了一个adjust-position属性默认值为true含义是键盘弹起时自动把页面往上推保证当前聚焦的输入框能露出来。问题就出在这个自动往上推上。它推的不是输入框而是整个页面的滚动位置。相当于舞台上的演员位置不变导演直接把整块舞台板抬起来找镜头。舞台上的灯光、道具、背景板自然全部错位。你精心写的position: fixed底部按钮、absolute定位的背景图、依赖视口单位计算的尺寸在可视区域变化加滚动位置变化这两重作用下出现各种鬼畜表现。这个根因决定了后面所有方案的方向要么让系统别瞎抬要么自己精准地抬。记住这句话后面所有方法都是围绕它展开的。1.2 三种典型乱象先对号入座我这些年接触到的键盘弹起布局问题基本可以归成三大类。第一类输入框在页面上部底部有个固定按钮。键盘弹起后按钮要么直接盖在输入框上要么被顶到键盘上方悬空。这是因为按钮用了position: fixed定位在底部webview可视高度一变fixed元素的底部基准就跟着跑到键盘顶部边缘去了。第二类页面上有多个输入框用户挨个填。每聚焦一个输入框页面就跳动一次滚动位置忽上忽下光标有时候跑到可视区外面。这其实是每次都触发一次自动上推造成的叠buff效果上一轮的滚动状态还没复位下一轮又来了。第三类自定义弹窗里有输入框。弹窗用view模拟全屏蒙层加居中卡片键盘一弹整个弹窗被推歪卡片跑到屏幕外或者底部的确认按钮正好被键盘挡住。弹窗本身就不是原生组件它同样受webview可视区域变化影响而adjust-position只保证输入框可见根本不管弹窗整体好不好看。这三类问题表面症状不同底层都是同一件事webview可视区域和滚动位置被系统改了但我们的固定定位、弹窗布局没有跟着适应。1.3 别忘了textarea和原生组件的边界还有一个容易被忽略的兄弟组件textarea。它的行为和input不完全一样。在微信小程序的历史版本里textarea是原生组件层级天然盖在普通view之上经常导致穿透、遮挡问题。现在虽然大部分场景已经同层渲染但键盘弹起时textarea的尺寸和滚动行为仍然有自己的一套逻辑。我的建议是表单里能用input就不用textarea。实在需要多行输入尽量让textarea保持固定高度不要开auto-height。开了auto-height之后输入内容变多组件高度跟着变键盘一弹高度变化和可视区域变化叠在一起排查起来能让人崩溃。另外cover-view这类原生组件覆盖层在键盘弹起时也可能出现错位。如果你在表单页使用了map、video这些原生组件并且上面盖了cover-view做按钮键盘弹起后这些覆盖层的位置计算同样要额外小心。但这不是今天的主角先提个醒遇到再说。2. 第一套组合拳用input自身属性稳住基础局面2.1 adjust-position与cursor-spacing的正确配比遇到键盘弹起布局乱先别急着写几百行监听代码。先把input组件自身的两个属性调明白基本能修好一半的简单页面。adjust-position布尔值默认true。控制键盘弹起时是否自动把页面上推。cursor-spacing数字单位px默认0。控制输入框底边到键盘顶边的距离。如果你只有一个输入框或者表单页结构简单我建议保留adjust-position为true把cursor-spacing调到15到20px之间。这样键盘弹起来后输入框不会紧紧贴着键盘顶部底部留一点呼吸空间视觉上舒服很多。如果你发现标题栏、顶部导航被顶飞了说明自动上推的副作用太大这时候再把adjust-position设为false。但千万记住一旦关了键盘不会帮你让路输入框有可能会被键盘挡住。adjust-positionfalse永远只是第一步后面必须接上手动的滚动处理否则只会从一种乱变成另一种乱。简单的表单页面参考这个模板template view classphone-form view classfield text classlabel手机号/text input classinput typenumber maxlength11 placeholder请输入手机号 :adjust-positiontrue :cursor-spacing20 / /view /view /template在输入框数量少、页面不超一屏的情况下这套配置基本不会出问题。如果页面很长用户需要上下滚动就不能只靠这两个属性了往下看第二节。2.2 容易被忽略的另外三个input属性除了上面两个还有三个属性对键盘体验影响很大却经常被忽略。hold-keyboard默认false。设为true时输入框聚焦后用户点击页面其他区域键盘不会自动收起。适合输入完马上点确认的页面比如验证码输入后立刻点登录避免键盘先收起来又弹起来造成视觉闪动。confirm-type设置键盘右下角按钮的文案。可选值有done、next、search、send等。表单页建议按业务语义设置搜索框用search前面几个输入框用next最后一个用done。配合键盘右下角按钮用户回车可以直接跳下一个输入框或提交体验会好很多。auto-blur默认false。设为true时点击键盘右下角按钮会自动失焦。如果有强制校验建议保持默认不要让它自动丢掉焦点否则校验还没跑键盘先收了交互节奏很怪。这里有一个经常踩的坑hold-keyboard在部分安卓机型上不稳定设置了不收起实际键盘却照收不误。我的兜底做法是在blur事件里调用一下uni.hideKeyboard()防止键盘意外收起时输入框还挂着focus状态导致样式出现异常。2.3 什么时候这套组合拳不够用讲清楚适用边界很重要。上面的属性组合本质上是把系统自动上推这件不可控的事稍微调教得可控一点。但它仍然不够精细。第一当键盘高度不固定时cursor-spacing的效果会打折。安卓第三方输入法键盘高度千奇百怪有的输入法还有表情栏、手写栏高度随时变系统没法稳定地按你设置的间距定位。第二当页面有吸顶元素或者复杂flex布局时自动上推会把整个结构顶散。比如顶部有自定义导航栏键盘一弹导航栏跟着起飞这种场景不是简单调属性就能解决的。第三当页面很长的表单需要滚动时自动上推只是保证当前输入框可见但不会帮你考虑页面整体滚动位置是否合理。用户甚至会出现在输入框A填完页面已经被推出去一屏完全找不到刚才填到哪了的尴尬情况。遇到这三种情况就该进入下一套方案关掉自动上推手动接管滚动。3. 第二套组合拳关掉自动上推手动精确滚动3.1 createSelectorQuery定位别让输入框被盖住当表单页超过一屏或者顶部有吸顶元素干扰时我会直接关闭adjust-position改成在输入框focus时自己滚动定位。关闭自动上推之后键盘弹出时页面基本保持不动输入框是否可见完全由你的代码决定。好处是行为可预测不会出现页面被乱推的副作用坏处是如果输入框本身位于屏幕底部键盘会把它挡住你必须手动把页面滚上去。定位的核心手段是uni.createSelectorQuery()。它能在真机上拿到元素相对于视口的位置配合uni.pageScrollTo()就能把页面精准滚动到指定位置。输入框聚焦时先查询输入框的boundingClientRect拿到它相对视口的位置再计算要滚动到哪里。代码示例function handleFocus() { // 等渲染层数据更新完成避免拿到旧的位置 uni.$nextTick(() { const query uni.createSelectorQuery().in(this) query.select(#phoneInput).boundingClientRect() query.selectViewport().scrollOffset() query.exec((res) { const rect res[0] const scroll res[1] if (!rect) return // 目标让输入框底部距离视口顶部约120px const target scroll.scrollTop rect.top - 120 uni.pageScrollTo({ scrollTop: Math.max(target, 0), duration: 200 }) }) }) }这里有个细节容易翻车createSelectorQuery()在uni-app的vue页面里建议使用.in(this)把查询作用域绑定到当前页面实例。如果不绑定在小程序中可能拿到组件内第一个匹配到的元素结果算出来的位置全是错的。3.2 多输入框切换和键盘动画的时序问题多个输入框时如果每个都在focus时执行滚动会出现一个更尴尬的现象用户从上面输入框切到下面输入框页面先滚到上面输入框的位置收到focus事件后再滚到下面输入框的位置视觉上就是一顿猛跳。我的解决思路是不在focus时马上滚而是加一个极短延迟让系统先完成键盘和焦点的状态切换再计算位置滚动。代码如下function handleFocus(e, currentIndex) { clearTimeout(this.scrollTimer) this.scrollTimer setTimeout(() { this.scrollToField(#field-${currentIndex}) }, 50) }50毫秒的延迟是为了避开键盘动画刚开始的抖动。实测在大部分机型上这个延迟已经够用而且用户不会觉得反应慢。还要注意滚动目标位置的选取。如果每次都把输入框滚到同一个位置体验虽然统一但页面顶部一直被卷走用户会觉得内容总是在跳。更好的做法是输入框本身在视口内的位置如果已经足够靠上就不必滚动只有它快被键盘遮挡时才滚动。判断条件参考const rect res[0] const viewportHeight res[1].windowHeight const inputBottom rect.bottom if (inputBottom viewportHeight * 0.6) { // 快被键盘挡住了执行滚动 }这个阈值0.6不是固定值可以根据视觉稿微调。我提它只是为了说明一个原则能不动就不动少动比多动舒服。手动滚动方案最大的价值就是让你重新获得了对页面位置的掌控权。3.3 失焦要不要还原位置的取舍失焦还原是个需要想清楚的产品问题。关闭adjust-position之后键盘收起时页面不会自动回滚到之前的位置。如果你希望用户填完某个输入框后视线回到页面顶部那得自己记住之前的scrollTop在blur时执行回滚。但我在实际项目里很少做还原。原因很直接用户填完一个字段大概率会继续填下一个或者点提交页面位置保持不变反而更连贯。只有在场景确实是输入完弹校验提示的时候才考虑回滚到聚焦前的位置。另外键盘收起本身也会触发一系列布局变化。如果你做了回滚最好也加一小段延迟等键盘完全收起来再滚否则视觉上还是会有一次跳动。判断键盘是否完全收起最靠谱的方式是监听键盘高度变化当高度归零时再执行回滚。这就自然引出了第三套组合拳。4. 第三套组合拳监听键盘高度搞定底部和弹窗4.1 onKeyboardHeightChange怎么用注意什么手动滚动能解决输入框被遮挡的问题但它有一个盲区我们不知道键盘到底多高。像底部固定按钮这种需求最优雅的方式是直接拿到键盘高度然后动态调整按钮位置。微信小程序提供了wx.onKeyboardHeightChangeuni-app包装成了uni.onKeyboardHeightChange。监听器回调里会返回当前键盘高度高度为0表示键盘完全收起。基本用法onLoad() { if (typeof uni.onKeyboardHeightChange function) { uni.onKeyboardHeightChange((res) { console.log(键盘高度变化, res.height) this.keyboardHeight res.height }) } }注意两点。第一这个API不是所有基础库版本都可用绑定前一定要判断typeof uni.onKeyboardHeightChange function否则老版本真机上直接报错。第二回调里不要做重度计算它触发频率不低尤其iOS某些版本在键盘动画过程中会连续回调多次。你只需要把高度值存下来样式上的动画交给CSS过渡去处理。4.2 底部固定按钮动态避让附安全区叠加拿到键盘高度后底部固定按钮的位置就非常直观了。假设原来是.submit-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 16rpx 32rpx; }现在改成动态绑定view classsubmit-bar :style{ bottom: keyboardHeight px } button typeprimary tapsubmit提交/button /view键盘弹起时keyboardHeight大约是键盘高度按钮就刚好停在键盘上方键盘收起时keyboardHeight变成0按钮回到页面底部。如果页面底部还有iPhone安全区需要把安全区高度一起叠加上去。最省事的方式是用CSS的env(safe-area-inset-bottom)view classsubmit-bar :style{ bottom: calc(${keyboardHeight}px env(safe-area-inset-bottom)) } 为了让按钮位置变化不那么生硬可以给submit-bar加一个300ms左右的CSS过渡.submit-bar { transition: bottom 0.3s ease; }不过这里有个反直觉的坑过渡动画在键盘高度连续上报时可能产生拖影按钮会一直抖。如果真机上出现这个表现直接把过渡去掉让底部跟随得更跟手反而比带动画舒服。4.3 弹窗输入框的三个可选方案和取舍自定义弹窗和键盘的组合是我最想劝退的场景。所有把输入框放进自定义弹窗的页面我都建议认真想想交互设计。方案一拆分独立页面。把需要在弹窗里输入的内容拆成一个独立页面比如备注、地址、昵称编辑全部做成新页面。这样键盘问题交给系统自动处理省心、稳定、代码简单。缺点是页面跳转比弹窗重一点但这在现代小程序里感知并不强。方案二动态padding避让。弹窗内容区域使用scroll-view外层根据键盘高度动态加padding-bottomview classmask v-ifvisible view classdialog :style{ paddingBottom: keyboardHeight px } scroll-view scroll-y classdialog-scroll input :adjust-positionfalse placeholder请输入备注 / /scroll-view /view /view加上键盘高度之后弹窗底部的内容不会被键盘盖住弹窗卡片本身不用移动位置视觉上更稳定。这个方案我在反馈弹窗里实测过比整体translateY的方式稳得多。方案三整体平移不推荐。网上很多教程会教你把弹窗容器用transform: translateY(-keyboardHeight)整体顶上去。这个方案在键盘高度不变时能用但键盘高度一变整个弹窗就像坐电梯一样上上下下观感很差。而且transform在某些安卓机型上会影响position: fixed的表现很容易引入新bug。我的最终选择是信息输入型的弹窗一律拆独立页面只有那种选一下的轻交互才用弹窗而且弹窗内原则上不出现输入框。这个产品决策能帮你省下大量调键盘的时间。5. 多端适配与工程化封装让方案可以复制5.1 微信、支付宝、H5、App四端行为差异uni-app的核心价值是write once run anywhere但键盘弹起这事恰恰是多端不一致的典型。你需要清楚每个端的能力边界才能写出不翻车的代码。微信小程序adjust-position和cursor-spacing都生效uni.onKeyboardHeightChange可用基础库2.7.0以上支持。整体可控性最好上面三套组合拳都能用。支付宝小程序input组件也有adjust-position属性但键盘高度监听事件的兼容性不如微信。建议在真机上单独验证不要在文档层面假设它可用。大部分场景下调好adjust-position和cursor-spacing就够用。H5端键盘弹起时页面高度不变视觉上底部会被键盘盖住但不会像小程序那样把整个webview重排得那么厉害。iOS上要注意Safari地址栏收起时视口高度变化的问题建议用100dvh这类新视口单位来处理。App端uni-app打包原生Appuni-app在App端提供了softinput配置在manifest.json里设置mode支持adjustResize和adjustPan分别对应压缩页面和上推页面。真机上表现和运行环境关系很大尤其安卓WebView版本不同行为差异很明显必须实测。我把差异整理成了一个表格方便你对照端自动上推键盘高度监听实际建议微信小程序支持支持2.7.0三套组合拳都用得上支付宝小程序支持需实测优先调input属性慎用全局监听H5不存在不存在用dvh/vh加底部padding处理App端可配置有限支持manifest配置加真机验证5.2 页面与工程配置disableScroll和softinput工程层面页面级配置同样关键。如果你确定某个表单页面不需要用户手动滚动可以在页面的page.json里加disableScroll: true禁止整页滚动。但注意一旦禁止滚动uni.pageScrollTo也会失效所以这个配置只适合单屏表单。我之前一度想靠这个配置一劳永逸结果用户从输入法退回桌面再回来页面滚动位置直接错乱最后不得不放弃。App端的manifest.json配置参考{ app-plus: { softinput: { mode: adjustResize, navigationBar: auto, input: { adjustPosition: true } } } }真机上如果发现App端键盘弹出把页面顶得过分可以把mode改成adjustPan。这个改动不需要发版直接自定义基座就能验证很适合调试期反复尝试。5.3 封装一个跨端键盘高度工具直接抄作业最后把前面这些经验封装成一个工具函数方便后续项目直接复用。我用Vue3的Composition API写了一份Vue2项目换成mixin逻辑就行。// useKeyboardHeight.js import { ref, onMounted, onUnmounted } from vue export function useKeyboardHeight() { const keyboardHeight ref(0) const isKeyboardShow ref(false) let timer null const handler (res) { // 部分机型高度连续上报做一下防抖 if (timer) clearTimeout(timer) timer setTimeout(() { keyboardHeight.value res.height || 0 isKeyboardShow.value keyboardHeight.value 0 }, 10) } onMounted(() { // #ifdef MP-WEIXIN if (typeof uni.onKeyboardHeightChange function) { uni.onKeyboardHeightChange(handler) } // #endif }) onUnmounted(() { if (timer) clearTimeout(timer) // #ifdef MP-WEIXIN if (typeof uni.offKeyboardHeightChange function) { uni.offKeyboardHeightChange(handler) } // #endif }) return { keyboardHeight, isKeyboardShow } }在表单页面里这样用script setup const { keyboardHeight, isKeyboardShow } useKeyboardHeight() /script template view classpage view classbottom-bar :style{ paddingBottom: isKeyboardShow ? keyboardHeight px : 0px } button确认/button /view /view /template整个页面不用再关心键盘到底多高只要拿到高度值所有fixed元素都能统一避让。如果项目里有多个表单页这工具能帮你把解决方案复制到任何页面。6. 真机问题速查与经验教训6.1 键盘弹起布局错乱速查表现象原因解决方案页面整体被顶起标题飞出屏幕adjust-position自动上推关闭自动上推改手动滚动底部fixed按钮被键盘挡住按钮固定在页面底部键盘遮挡监听键盘高度动态抬高按钮输入框被键盘挡住光标不可见cursor-spacing太小或关闭了自动上推设置cursor-spacing20或手动滚动多输入框切换页面跳动每次focus都触发自动上推统一管理滚动逻辑加延迟再去算位置弹窗输入框弹起时弹窗乱跑弹窗是普通view受键盘影响动态padding避让或拆成独立页面键盘收起后页面不回滚关闭自动上推后没有自动恢复blur时记录scrollTop手动恢复安卓机hold-keyboard不稳定不同安卓WebView行为差异blur时uni.hideKeyboard()兜底弹窗底部按钮被键盘盖住弹窗footer在可视区底部弹窗内容区加padding-bottom: keyboardHeight6.2 从表象到根因的排查顺序遇到键盘弹起布局乱先别急着写代码。我建议按下面的顺序定位能省不少时间。第一步把页面里所有使用position: fixed的元素列出来看它们是不是键盘弹起后的主要受害者。是的话优先考虑键盘高度监听方案。第二步看输入框是否被键盘遮挡。是的话先试cursor-spacing无效就关adjust-position手动滚动。第三步区分是页面滚动位置错乱还是元素尺寸布局错乱。前者是webview滚动问题后者可能是百分比高度或flex布局在可视区域变化后重新计算导致的。我排过最诡异的一个bug页面根节点设置了height: 100%键盘弹起后可视高度变化100%重新计算导致内部所有fixed元素跟着动。这种问题靠监听键盘高度很难根除必须把根节点改成min-height: 100vh或者固定高度让根节点不要参与视口高度联动。6.3 写在最后几件提升测试效率的小事开发者工具的模拟键盘是假的它不会触发真机的软键盘动画和webview resize。我踩过最大的坑就是在开发者工具里调得完美一跑iPhone全乱。现在我的习惯是所有涉及键盘的改动先在开发者工具里把逻辑调通然后立刻拿iPhone和安卓真机各走一遍流程两端都过了才算完成。测试时要覆盖三种键盘形态系统原生输入法、第三方输入法、手写键盘。这三者高度差异很大同一个页面在不同键盘下表现完全不一样。如果你做的App用户群体里有大量安卓用户这点尤其重要。还有一个长效机制把键盘弹起布局专项测试写进团队测试用例模板。表单页面每次迭代都要过一遍这个用例比每次出了线上问题再救火高效得多。个人经验总结这个问题没有银弹但组合拳一定有效先调cursor-spacing再决定要不要关adjust-position最后用键盘高度监听解决fixed元素三层递进能覆盖绝大多数场景。如果你能接受弹窗输入框拆独立页面这个产品决策还能再省下相当多的时间。最后送大家一句话键盘弹起布局问题绝大多数不是样式写错而是没搞清楚webview在键盘出现的那一刻到底发生了什么。顺着这个思路去排查你很快会发现那些乱跳的页面其实都遵循着同一套逻辑。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →