Android 16 WebView 页面顶部挖孔/通知栏不能显示UI问题排查

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 逐一尝试无效的方案

  1. 改 CSS padding (菜单 8vh → max(12px, env()) → 12px):只移动了视口内的内容,色带依旧。❌
  2. 挖孔模式 shortEdges → always(三处:values / values-v35 / Manifest / 运行时):编译值确认 = always,无效。❌
  3. WebView 层拦截 insetssetOnApplyWindowInsetsListener 消费 statusBars+cutout):无效------Chrome 不走 dispatch 更新。❌
  4. WebView 层拦截后把清零 insets 喂回默认处理v.onApplyWindowInsets(zeroed)):无效------Chrome 直接读窗口根 insets。❌
  5. 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 排布的内容会随容器整体上移,不需要逐元素处理。

六、诊断方法论(可复用)

  1. 版本号升 versionName/versionCode:让用户能在「设置→应用」里确认装的是哪个版本,避免"改了没生效"的假象。
  2. 页面诊断浮层 :打印 innerHeight / clientHeight / visualViewport.offsetTop / body.getBoundingClientRect().top / env(safe-area-inset-top) / dpr,一次截图拿到全部关键数值。
  3. 先确认 dpr 再换算物理像素:1440px 按 dpr=3 算是 480dp,实际 dpr=4 是 360dp------换算出 53px 与 40px 的差别直接决定了修复方向。
  4. 判别色实验:把 WebView 背景与窗口背景设成不同颜色,看色带"漏出"的是哪一层的颜色,一图确认"谁覆盖了谁"。
  5. 区分"视图被顶下来"与"视口被收缩"
    • 色带 = 窗口背景色 → WebView 视图被顶下来(decor 内边距问题)
    • 色带 = WebView 表面色 → WebView 全屏,Chrome 收缩了页面视口
  6. 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%

修复后效果:菜单大标题从屏幕最顶端开始,挖孔压在标题上沿;对局页顶栏同样贴顶。

相关推荐
weixin_727535622 小时前
Loop 已死,Graph 新生:AI 工作流的范式革命
android·人工智能·rxjava
严同学正在努力3 小时前
从备份到恢复:我用 30 分钟恢复了误删的核心业务表
android·java·数据库·ai
梦想三三3 小时前
LangChain Output Parser 实战:从字符串到结构化数据的完整指南
android·服务器·langchain·github·uv
爱笑鱼5 小时前
Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?
android
Android-Flutter6 小时前
Kotlin 冷流与热流详解
android·kotlin
zhangphil6 小时前
Android ContentProvider/ContentResolver读图片,跨进程 IPC慢
android
yueqc16 小时前
Android .so 文件压缩
android·so·包体积
龚礼鹏7 小时前
深入解析 Android Automotive (AAOS) 启动流程与 CarService 核心机制(基于 Android 16 最新源码视角)
android
rosmis7 小时前
agent各指标定义
android·java·开发语言