- android: remaining commercialization tasks, WeChat-style timestamp divider (no rules), send auto-scroll, failed-message retry, FeatureGate, avatar - backend: fix Edge TTS reading SSML tags aloud — edge_tts.Communicate escapes input, so custom SSML was spoken as text; now plain text + rate param (_build_ssml/_VOICE_STYLE_MAP removed); add custom_presets & push_greetings APIs/models; alembic 029-032 + post-restore sync migrations 1ab200d2d94d/a51ee94d762d with all DROP ops disabled - frontend: AgentChat/company view updates, TTS markdown stripping - docs: TTS bugfix record, Android fix docs - repo hygiene: remove 25 committed tts_outputs mp3 artifacts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.2 KiB
Android 软键盘遮挡输入框 — 修复执行文档
日期:2026-08-05 ・ 现象:聊天页弹起软键盘后,底部发送栏被键盘顶边裁掉约 1/3(见需求方截图:chips 条完整露出,输入胶囊下半截压在键盘下面) 执行者:deepseek ・ 预计改动:3 处,共约 15 行
一、根因(已查证,不是猜测)
三处配置叠加导致:
-
AndroidManifest.xml没有配windowSoftInputModeMainActivity 未声明,系统默认行为是adjustPan(平移而非缩放布局)。 -
MainActivity.kt:39用了enableEdgeToEdge()边到边模式下系统不再帮你 resize,必须由 Compose 代码自己消费 IME insets。 -
ui/chat/ChatScreen.kt全文件没有任何imePadding()(grep 实证) 新做的底部发送栏(胶囊输入条 + chips 条)没有消费键盘 insets → 键盘弹出时栏体只被 adjustPan 平移了一部分,下半截被键盘盖住。
二、修复点
修复 1:Manifest 声明 adjustResize(必须)
android/app/src/main/AndroidManifest.xml,MainActivity 节点加:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
... />
这是 IME insets 能正确分发到 Compose 的前提,缺了它修复 2 在某些机型上仍不生效。
修复 2:底部容器整体加 imePadding(核心)
ChatScreen.kt 的 bottomBar(约 428 行起):把 chips 条 + 停止生成胶囊 + 排队提示 + 输入栏 视为一个整体,在其最外层容器(Column 或 Surface)上加:
Modifier
.navigationBarsPadding() // 先吃手势导航条
.imePadding() // 再吃键盘;键盘弹出时它会覆盖/包含导航条高度
注意:
- 顺序不能反(先 navigationBars 后 ime);
- 加在整个 bottomBar 内容的最外层,不要只加在输入胶囊上——否则键盘弹起时 chips 条会被输入栏顶出重叠;
- 如果 Scaffold 的
contentWindowInsets被改过,恢复默认,避免 insets 被 Scaffold 提前消费导致 imePadding 拿不到。
修复 3:键盘弹起时消息列表联动(体验收尾)
bottomBar 被键盘顶高后,Scaffold 的 paddingValues 会变大,LazyColumn 底部留白随之增大——需要让列表保持"贴着最新消息":
// ChatScreen 内,listState 已存在
val imeBottom = WindowInsets.ime.getBottom(LocalDensity.current)
LaunchedEffect(imeBottom) {
if (imeBottom > 0 && uiState.messages.isNotEmpty()) {
listState.animateScrollToItem(listState.layoutInfo.totalItemsCount) // 滚到底
}
}
效果对齐微信/豆包:键盘起来 → 输入栏完整贴键盘上沿 → 消息列表自动滚到最后一条。
不要做的事
- 不要给 LazyColumn 再加一份 imePadding(bottomBar 顶上来后 contentPadding 已经变大,双份会留白过多);
- 不要改用
adjustPan方案糊弄——平移方案在 edge-to-edge 下表现不一致; - chips 条的显示/隐藏逻辑(流式时隐藏)不要动。
三、验收清单(真机逐项过)
| # | 操作 | 通过标准 |
|---|---|---|
| 1 | 点输入框弹键盘 | 输入胶囊完整露出,下沿贴键盘顶边,无裁切 |
| 2 | 键盘弹起瞬间 | chips 条随底栏整体上移,不被输入栏覆盖、不与消息区重叠 |
| 3 | 键盘弹起后 | 消息列表自动滚到最新一条;列表不被键盘遮挡可读 |
| 4 | 收起键盘 | 底栏回落到导航条上方,无残留空白、无跳动 |
| 5 | 输入 5 行文字 | 输入框自增高,栏体始终完整在键盘上方 |
| 6 | 手势导航 vs 三键导航两种模式 | 键盘收起时底栏不被导航条压住 |
| 7 | 流式回复中弹键盘 | "停止生成"胶囊与输入栏布局不错乱 |
| 8 | 横屏(如支持) | 不崩、不全屏输入态遮挡即可 |
四、回归风险
- 改动只涉及 Manifest 一行 + ChatScreen 底部容器修饰符 + 一个 LaunchedEffect,不影响消息流、SSE、语音逻辑;
- 重点回归:语音"按住 说话"模式(overlay 与键盘互斥——录音 overlay 弹出时若键盘还开着要先收键盘)、图片选择返回后的布局、深色模式。