Android 16 WebView 页面视口收缩问题排查经验
项目:斗兽棋(Capacitor 8 + HTML 单页游戏,targetSdk 36) 日期:2026-08-02 | 结论:已修复(v1.10)
一、问题现象
游戏 App 在手机上运行时,屏幕顶部(通知栏/挖孔区域)出现约 40dp 的纯色带,游戏 UI 整体下移:
- 菜单大标题没有贴顶,顶部留出一条纯深绿色背景带
- 对局页顶栏(菜单按钮/标题/声音按钮)同样被整体下移
- 状态栏图标不可见(沉浸式隐藏生效),色带里只有背景色
- 看起来违和、浪费屏幕空间

二、运行环境
| 项 | 值 |
|---|---|
| 手机 | OPPO/OnePlus PHY110,Android 16.0.9.400 (CN01),挖孔屏 |
| 屏幕 | 1440×3168 物理像素,dpr = 4 → CSS 视口 360×792 |
| 技术栈 | Capacitor 8.5,Android WebView(Chromium),targetSdk 36,minSdk 24 |
| 应用 | 全屏沉浸式游戏(原生隐藏系统栏 + edge-to-edge) |
三、排查过程(走过的弯路)
3.1 已确认生效的配置(却不是根因)
| 配置 | 结论 |
|---|---|
WindowCompat.setDecorFitsSystemWindows(window, false) |
窗口已 edge-to-edge ✅ |
系统栏全透明 + 沉浸式隐藏(WindowInsetsControllerCompat.hide) |
状态栏确实隐藏(无图标)✅ |
windowLayoutInDisplayCutoutMode=always(主题 + Manifest + 运行时强制) |
已用 aapt2 验证编译值 = 3(always)✅ |
viewport-fit=cover meta 标签 |
已存在 ✅ |
以上全部正确,但色带依旧------说明问题不在这些层面。
3.2 逐一尝试无效的方案
- 改 CSS padding (菜单 8vh →
max(12px, env())→ 12px):只移动了视口内的内容,色带依旧。❌ - 挖孔模式 shortEdges → always(三处:values / values-v35 / Manifest / 运行时):编译值确认 = always,无效。❌
- WebView 层拦截 insets (
setOnApplyWindowInsetsListener消费 statusBars+cutout):无效------Chrome 不走 dispatch 更新。❌ - WebView 层拦截后把清零 insets 喂回默认处理 (
v.onApplyWindowInsets(zeroed)):无效------Chrome 直接读窗口根 insets。❌ FLAG_LAYOUT_NO_LIMITS(窗口忽略所有 inset 布局):无效。❌
3.3 关键转折:页面诊断浮层
在页面里加一个 position:fixed;top:0 的浮层,打印视口数值(内联在页面脚本里):
js
innerH=752 innerW=360 // window.innerHeight / innerWidth
clientH=752 clientW=360 // documentElement.clientHeight
visualVp.offsetTop=0 vpH=752 // visualViewport
body.top=0.0 body.h=752.0 // body 相对视口的位置
menu.top=0.0 menu.paddingTop=12px
safe-inset-top=0 // env(safe-area-inset-top)
dpr=4
结论:页面视口 = 360×752,而屏幕 = 360×792------视口正好比屏幕矮 40dp。 浮层本身 top:0 却出现在 160 物理像素处,证明 fixed 元素的定位基准(视口)整体下移了 40dp。
⚠️ 教训:最初按 dpr=3 把 160px 误算成 53dp,实际 dpr=4 → 40dp 。先确认
devicePixelRatio再换算!
3.4 关键转折:红蓝判别色实验
把 WebView 背景设为红色、窗口背景设为蓝色(v1.9),顶部色带颜色即暴露"哪一层露出来":

结果:色带是深绿 (13,26,17)(Capacitor 配置的 WebView 背景色 #0d1a12),不是红色也不是蓝色。
→ WebView 视图本身全屏铺满;是 Chrome 把页面视口按窗口根 insets(顶部安全区 40dp)收缩了。
四、根因分析
- Android 15/16 强制 edge-to-edge 后,WebView 页面视口按窗口根 insets 收缩------顶部安全区(状态栏 + 挖孔)40dp 被计入视口外,页面只能在下方 752dp 内排版;
viewport-fit=cover在该 WebView 上未生效(已知 Chromium WebView 的 safe-area/视口处理问题,部分版本才修复);- Chrome 读取的是窗口根 insets(
getRootWindowInsets),不是 dispatch 链------所以在 WebView 上挂 inset 监听器、消费 insets、喂回清零值全都无效; FLAG_LAYOUT_NO_LIMITS只影响窗口布局,不影响 Chrome 对根 insets 的读取。
五、最终解决方案(v1.10,双保险)
5.1 原生:decor 根节点消费系统栏 insets(治本)
java
// MainActivity.onCreate 与 onWindowFocusChanged 都调用
private void consumeSystemBarsAtRoot() {
getWindow().getDecorView().setOnApplyWindowInsetsListener((v, insets) ->
insets.consumeSystemWindowInsets());
}
原理:在根节点就把 insets 消费掉 → WebView 收到的 insets 为零 → Chrome 认为没有系统栏 → 视口铺满。注意必须挂在 decor(根)上,挂在 WebView 上无效(见 3.2)。
5.2 网页 JS 偏移补偿(兜底,保证视觉效果)
js
(function(){
try{
const fullH = Math.max((window.screen && screen.height)||0,
window.outerHeight || 0,
window.innerHeight || 0);
const off = fullH - window.innerHeight; // 本机 = 792-752 = 40
if (off > 0) {
const els = document.querySelectorAll('.screen,.overlay');
for (let i = 0; i < els.length; i++) els[i].style.top = (-off) + 'px';
}
}catch(e){}
})();
原理:fixed 全屏层相对视口定位,视口矮 40dp 所以整体下移 40dp;检测偏移量并把全屏层上移后,内容即铺满屏幕顶。原生修复生效时 off = 0,此段自动无操作。
注意:只补偿全屏层(
position:fixed;inset:0的容器),容器内部用 padding 排布的内容会随容器整体上移,不需要逐元素处理。
六、诊断方法论(可复用)
- 版本号升 versionName/versionCode:让用户能在「设置→应用」里确认装的是哪个版本,避免"改了没生效"的假象。
- 页面诊断浮层 :打印
innerHeight / clientHeight / visualViewport.offsetTop / body.getBoundingClientRect().top / env(safe-area-inset-top) / dpr,一次截图拿到全部关键数值。 - 先确认 dpr 再换算物理像素:1440px 按 dpr=3 算是 480dp,实际 dpr=4 是 360dp------换算出 53px 与 40px 的差别直接决定了修复方向。
- 判别色实验:把 WebView 背景与窗口背景设成不同颜色,看色带"漏出"的是哪一层的颜色,一图确认"谁覆盖了谁"。
- 区分"视图被顶下来"与"视口被收缩" :
- 色带 = 窗口背景色 → WebView 视图被顶下来(decor 内边距问题)
- 色带 = WebView 表面色 → WebView 全屏,Chrome 收缩了页面视口
- aapt2 验证编译结果 :
aapt2 dump xmltree/dump resources确认属性真的编译进包(如挖孔模式 always=3),排除"改了但没编进去"。
七、经验总结
- 全屏 WebView 适配的检查顺序:edge-to-edge → 挖孔模式 → 根 insets 消费 → 页面兜底;
- 不要在 WebView 层拦截 insets 期望 Chrome 生效------Chrome 读窗口根 insets;
- 每次改动让用户重装测试成本很高,原生治本 + 页面兜底双保险一次打包,避免多轮往返;
- 相关已知问题:Chromium WebView 的 safe-area 处理(Tauri issue #14240)、Android 16 强制 edge-to-edge(WebView edge-to-edge 适配指南)、挖孔屏适配(刘海屏/挖孔屏适配详解)。
八、关联修复(同会话完成)
| 问题 | 修复 |
|---|---|
| 顶部色带(本文) | v1.10:decor 根消费 insets + JS 偏移补偿 |
| 困毙判胜无说明 | v1.3:结果弹窗显示"对方无棋可动(困毙),你获胜!" |
| 图标边角裁剪过多 | v1.2:原图缩至自适应画布 62% 贴深绿底,可见度 35%→87% |
修复后效果:菜单大标题从屏幕最顶端开始,挖孔压在标题上沿;对局页顶栏同样贴顶。