UniApp z-paging 聊天页:安卓键盘弹起输入框飞顶与底部空白
用 z-paging 的 use-chat-record-mode 做问答页时,iOS 表现正常,安卓一聚焦就翻车:输入框跑到标题正下方,中间空出一大块白,底部 tabbar 还贴在键盘上面。
官方聊天 demo 看起来只有几行配置,真机却踩了不少坑。本文记录完整排查与最终落地方式。
问题现象
聚焦输入框、软键盘弹出后:
- 自定义导航栏仍在顶部
- 输入框紧贴导航栏下方(本应在键盘上方)
- 输入框与底部之间有一块接近键盘高度的空白
- 自定义 tabbar 仍显示在键盘上方
收起键盘后布局恢复。iOS 始终正常:导航栏不动,中间消息列表被挤压,输入框贴在键盘上方。
页面结构(踩坑前提)
这不是普通二级页,而是 自定义 tabBar 页:
z-paging+use-chat-record-mode#top:自定义 Navbar#bottom:textarea+ 为自定义 tabbar 预留的约65px占位- 根组件里还有一层
position: fixed的自定义 tabbar
<z-paging use-chat-record-mode safe-area-inset-bottom>
<template #top>
<CustomNavbar />
</template>
<!-- 消息列表 -->
<template #bottom>
<view>
<textarea
v-model="inputText"
auto-height
:adjust-position="false"
/>
<!-- 避开自定义 tabbar -->
<view class="h-[65px] pb-safe" />
</view>
</template>
</z-paging>
官方约定其实很简单
z-paging 2.7+ 聊天模式的设计是:
- 输入框设
:adjust-position="false",不要让系统自动顶页面 - 打开默认的
auto-adjust-position-when-chat,由 z-paging 在#bottom下方加一段 键盘高度占位 - flex 布局挤压中间列表,导航栏留在顶部
所以 demo 代码很少。前提是:只有 z-paging 在做避让。
根因:双重避让
安卓上常常是两套机制同时生效:
| 机制 | 行为 |
|---|---|
系统 softinputMode | 默认多为 adjustPan(整页上推),或 adjustResize(压缩 WebView) |
z-paging auto-adjust-position-when-chat | 再在底部插入 keyboardHeight 高度的占位 |
结果就是:页面已经被系统挪过一次,z-paging 又「再抬」一次。
截图里输入框下面的大块空白,基本就是 z-paging 的键盘占位高度;输入框被顶到标题下面,是双重抬升后的视觉结果。
对应源码逻辑(示意):
// z-paging chat-record-mode
this.keyboardHeight = res.height > 0
? res.height + this.finalChatAdjustPositionOffset
: res.height
<!-- 插在 slot=bottom 下方 -->
<view :style="{ height: keyboardHeight + 'px' }" />
排查过程(走过的弯路)
1. 以为是 textarea 的 fixed
早期在 textarea 上写了 fixed。文档说:父级是 position: fixed 时才需要。我们的 #bottom 在默认非页面滚动模式下是 flex 贴底,不是 fixed。
删掉 fixed 后现象依旧。 说明主因不是它。
2. softinputMode: 'nothing'
HTML5+ Webview 支持 'adjustPan' | 'adjustResize' | 'nothing',语义是「不做调整」。注意:
- 安卓原生叫
adjustNothing,uni-app / HTML5+ 里写的是nothing - pages.json 官方文档只明确写了
adjustResize/adjustPan - 在 vue 页写
nothing经常不生效
运行时 webview.setStyle({ softinputMode: 'nothing' }) 也没稳住。
3. 误用编译期 isAppAndroid
一度改用 @uni-helper/uni-env 的 isAppAndroid。它看的是编译期 UNI_APP_PLATFORM。
项目脚本是 uni -p app(一份 JS 双端跑)时,该常量在产物里恒为 false。于是安卓真机上「关 z-paging 占位」整段被跳过——表现成:代码还在、回滚也没用。
必须用运行时 uni.getSystemInfoSync().platform === 'android'。
4. 以为一定要运行时 setStyle
排查中加过对本页 / parent webview 的 setStyle({ softinputMode: 'adjustResize' })。后来在运行时 platform 判断正确的前提下删掉这段,真机仍然正常。
说明:pages / definePage 里的 softinputMode: 'adjustResize' 已经够用;之前「删了 setStyle 就坏」多半是平台判断失效叠出来的假象。
5. GitHub 上的临时方案
uni-z-paging#259 里有人提:
设置
chat-adjust-position-offset为-键盘高度
本质是把占位算成 kb + (-kb) = 0,和安卓关掉 auto-adjust-position-when-chat 效果接近,还要动态绑高度,并不更简单。
另有人说换 editor 组件。对「发一句话」的聊天框成本偏高,本文未采用。
最终方案
按平台拆开,只让 一套 机制负责避让:
| iOS | Android | |
|---|---|---|
| 系统 softinput | 不依赖 | pages 配置 adjustResize |
| z-paging 键盘占位 | 开 | 关(运行时判断平台) |
| 自定义 tabbar | 键盘弹起时隐藏 | 同左 |
| bottom 的 65px 占位 | 键盘弹起时收起 | 同左 |
| Navbar | #top 内非 fixed | 同左 |
1)页面配置 softinputMode
definePage({
style: {
navigationStyle: 'custom',
'app-plus': {
softinputMode: 'adjustResize',
},
},
})
2)运行时判断平台,安卓关掉 z-paging 占位
const isAndroidAppRuntime = (() => {
// #ifdef APP-PLUS
try {
return uni.getSystemInfoSync().platform.toLowerCase() === 'android'
}
catch {
return false
}
// #endif
// #ifndef APP-PLUS
return false
// #endif
})()
const autoAdjustPositionWhenChat = !isAndroidAppRuntime
<z-paging
use-chat-record-mode
:auto-adjust-position-when-chat="autoAdjustPositionWhenChat"
@keyboard-height-change="onKeyboardHeightChange"
>
3)键盘弹起时隐藏自定义 tabbar
// tabbar store
keyboardOpen: false,
setKeyboardOpen(open: boolean) {
this.keyboardOpen = open
}
<!-- App 根组件 -->
<CustomTabbar v-if="isCurrentPageTabbar && !tabbarStore.keyboardOpen" />
function onKeyboardHeightChange(res: { height: number }) {
tabbarStore.setKeyboardOpen(res.height > 0)
}
bottom 里的 65px 占位同步收起:
<view :class="keyboardOpen ? 'h-0' : 'h-[65px] pb-safe'" />
4)Navbar 不要在 #top 里再 fixed
<template #top>
<CustomNavbar :fixed="false" :placeholder="false" />
</template>
5)输入框
<textarea
v-model="inputText"
auto-height
:adjust-position="false"
:show-confirm-bar="false"
confirm-type="send"
/>
不要再挂无意义的 fixed。
为什么官方 demo「不用这么处理」
- demo 多半是普通页面,没有自定义 tabbar + 底部 65px 占位
- 约定是「
adjust-position=false+ z-paging 独占避让」,环境干净时几行就够 - 安卓上官方 demo 也会出现同类空白问题(见 issue #259)
我们这页难在:tabBar 页 + 自定义 fixed tabbar + 系统 softinput + z-paging 占位 叠在一起;再加上误用编译期平台常量,排查会被带偏。
小结
- 空白 ≈ z-paging 键盘占位;飞顶 ≈ 系统避让 + 占位叠加
- pages 用官方的
adjustResize/adjustPan,别指望nothing/adjustNothing - 平台判断用运行时
platform === 'android',不要用@uni-helper/uni-env的isAppAndroid(uni -p app双端同包时编译期恒为 false) - 安卓:
softinputMode: 'adjustResize'+ 关auto-adjust-position-when-chat;iOS:保留 z-paging 占位 - 运行时
webview.setStyle不是必须(判断正确后可删) - 聊天页键盘弹起时藏自定义 tabbar,并去掉对应占位
一次只让一个角色负责键盘避让,这类布局问题会少很多。