fix(键盘): 统一修复 Android 软键盘遮挡输入框
Docker Build and Push / build-image (push) Successful in 3m52s

根因: Tauri targetSdk 37 强制 edge-to-edge, MainActivity 不处理 IME Insets,
系统不再因键盘缩小窗口, 清单 adjustResize 失效, 键盘直接盖住 WebView。

- 新增 scripts/ci/android-softinput.sh: init 后注入 windowSoftInputMode=adjustResize,
  并重写 MainActivity.kt 监听 IME Insets, 键盘可见时把高度加到内容容器底部 (API 30+),
  让 WebView 真正变矮, 所有 bottom 弹层自动上移
- CI (android-build.yml) 与本地 build-apk.sh 在 init 之后、build 之前调用该脚本 (幂等)
- 新增全局 useKeepInputVisible: 输入框聚焦后用 scroll-into-view-if-needed 滚入可视区,
  覆盖页面滚动与 Popup 内部滚动
- 弹层补 max-height + 内部滚动, 防止键盘弹出后顶部被顶出屏幕
- calibrateSafeTop 在键盘弹出时保留旧值, 避免误判导致整页跳动
This commit is contained in:
jif2.zhang
2026-11-09 16:25:11 +08:00
parent 3730e55e93
commit 7b52220aad
9 changed files with 218 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# CI / 本地共用: 修复 Android 软键盘遮挡输入框
# 在 `tauri android init` 之后、`tauri android build` 之前运行。
#
# 背景:
# Tauri 2 生成的 MainActivity 调用 enableEdgeToEdge(), 且不处理 IME Insets。
# Android 15+ (targetSdk 35+) 强制 edge-to-edge 后, 系统不再因软键盘缩小窗口,
# 清单里的 adjustResize 也随之失效 → 键盘直接盖住 WebView, 底部弹层输入框被遮挡。
#
# 做法:
# 1. AndroidManifest.xml 的 MainActivity 补 windowSoftInputMode="adjustResize"
# (兼容 API < 35 的旧行为)
# 2. 重写 MainActivity.kt: 监听 WindowInsetsCompat.Type.ime(), 键盘可见时
# 把 IME 高度作为底部内边距加给内容容器 (等价恢复旧版 adjustResize),
# 这样 WebView 真正变矮, 所有 position:fixed / bottom:0 的弹层自动上移。
#
# 幂等: 重复执行不会重复注入。
# 注意: gen/android 被 gitignore, CI 每次 init 都重新生成, 所以每次构建都要跑本脚本。
set -euo pipefail
MANIFEST="src-tauri/gen/android/app/src/main/AndroidManifest.xml"
MAIN_ACTIVITY_DIR="src-tauri/gen/android/app/src/main/java"
if [ ! -f "$MANIFEST" ]; then
echo "[softinput] ERROR: 找不到 $MANIFEST (请先运行 tauri android init)" >&2
exit 1
fi
MAIN_ACTIVITY=$(find "$MAIN_ACTIVITY_DIR" -name MainActivity.kt -print -quit 2>/dev/null || true)
if [ -z "$MAIN_ACTIVITY" ]; then
echo "[softinput] ERROR: 找不到 MainActivity.kt (请先运行 tauri android init)" >&2
exit 1
fi
echo "[softinput] 注入 Android 软键盘避让配置..."
python3 - "$MANIFEST" "$MAIN_ACTIVITY" <<'PYEOF'
import re
import sys
manifest_path, activity_path = sys.argv[1], sys.argv[2]
# ---------- 1. AndroidManifest.xml: 补 windowSoftInputMode ----------
src = open(manifest_path, encoding='utf-8').read()
if 'windowSoftInputMode' not in src:
m = re.search(r'<activity\b', src)
if not m:
print('[softinput] ERROR: AndroidManifest.xml 中找不到 <activity>', file=sys.stderr)
sys.exit(1)
src = src[:m.end()] + '\n android:windowSoftInputMode="adjustResize"' + src[m.end():]
open(manifest_path, 'w', encoding='utf-8').write(src)
print('[softinput] AndroidManifest.xml: 已注入 windowSoftInputMode="adjustResize"')
else:
print('[softinput] AndroidManifest.xml: 已有 windowSoftInputMode, 跳过')
# ---------- 2. MainActivity.kt: IME Insets 处理 ----------
src = open(activity_path, encoding='utf-8').read()
if '[iboard] keyboard-insets' in src:
print('[softinput] MainActivity.kt: 已注入键盘避让, 跳过')
sys.exit(0)
m = re.search(r'^\s*package\s+([\w.]+)\s*$', src, flags=re.M)
if not m:
print('[softinput] ERROR: MainActivity.kt 中找不到 package 声明', file=sys.stderr)
sys.exit(1)
package = m.group(1)
kotlin = f'''package {package}
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
class MainActivity : TauriActivity() {{
override fun onCreate(savedInstanceState: Bundle?) {{
enableEdgeToEdge()
super.onCreate(savedInstanceState)
// [iboard] keyboard-insets
// Android 15+ 强制 edge-to-edge 后, 系统不再因软键盘缩小窗口, adjustResize 失效,
// 键盘会直接盖住 WebView 底部。这里监听 IME Insets, 键盘可见时把键盘高度
// 作为内容容器的底部内边距, 等价恢复旧版 adjustResize 行为。
// API < 30 由清单里的 adjustResize 处理, 避免系统已缩小窗口时重复留白。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {{
val content = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(content) {{ view, insets ->
// 键盘隐藏时 getInsets(ime).bottom 为 0 (AndroidX 在 API < 30 也做了兼容)
val imeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
view.setPadding(0, 0, 0, imeBottom)
insets
}}
ViewCompat.requestApplyInsets(content)
}}
}}
}}
'''
open(activity_path, 'w', encoding='utf-8').write(kotlin)
print(f'[softinput] MainActivity.kt: 已注入 IME Insets 处理 ({activity_path})')
PYEOF
# 断言: 防止注入被模板覆盖或路径写错
if ! grep -q 'windowSoftInputMode' "$MANIFEST"; then
echo "[softinput] ERROR: AndroidManifest.xml 注入未生效" >&2
exit 1
fi
if ! grep -q '\[iboard\] keyboard-insets' "$MAIN_ACTIVITY"; then
echo "[softinput] ERROR: MainActivity.kt 注入未生效" >&2
exit 1
fi
echo "[softinput] 完成"