踩坑··

UniApp z-paging 聊天页:安卓键盘弹起输入框飞顶与底部空白

z-paging 聊天记录模式在安卓上聚焦后输入框顶到标题下方、中间留出整块空白。根因是系统 softinputMode 与 z-paging 键盘占位双重避让,本文记录排查路径与最终方案。

用 z-paging 的 use-chat-record-mode 做问答页时,iOS 表现正常,安卓一聚焦就翻车:输入框跑到标题正下方,中间空出一大块白,底部 tabbar 还贴在键盘上面。

官方聊天 demo 看起来只有几行配置,真机却踩了不少坑。本文记录完整排查与最终落地方式。

问题现象

聚焦输入框、软键盘弹出后:

  1. 自定义导航栏仍在顶部
  2. 输入框紧贴导航栏下方(本应在键盘上方)
  3. 输入框与底部之间有一块接近键盘高度的空白
  4. 自定义 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+ 聊天模式的设计是:

  1. 输入框设 :adjust-position="false",不要让系统自动顶页面
  2. 打开默认的 auto-adjust-position-when-chat,由 z-paging 在 #bottom 下方加一段 键盘高度占位
  3. 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 组件。对「发一句话」的聊天框成本偏高,本文未采用。

最终方案

按平台拆开,只让 一套 机制负责避让:

iOSAndroid
系统 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「不用这么处理」

  1. demo 多半是普通页面,没有自定义 tabbar + 底部 65px 占位
  2. 约定是「adjust-position=false + z-paging 独占避让」,环境干净时几行就够
  3. 安卓上官方 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,并去掉对应占位

一次只让一个角色负责键盘避让,这类布局问题会少很多。