WebView 桥把 native 物理像素当 CSS px(Android 壳顶部留空位)
症状
在线壳 APK(WebView 加载线上站点)顶端留一截空位:内容被整体下压,顶部出现一段与状态栏高度相关的空白带。同一套前端在桌面浏览器完全正常——只有 APK 里出现。gap 高度随设备密度变化。
触发条件
- Android 在线壳:
addJavascriptInterface注入能力桥,桥把 native 测得的系统栏 inset 返回给前端; - 前端把桥返回值落成 CSS 变量当长度用(如
--safe-area-top: <n>px); - 设备密度 ≠ 1(真机 density 2.6~3.0,mdpi 模拟器 density=1 时不触发,这就是「第一次打 APK 没事、换真机后出事」的来源)。
根因
单位的隐性错位,跨过一次 JS 桥边界就丢掉了换算:
| 环节 | 单位 |
|---|---|
WindowInsets.getInsets(statusBars()).top / getSystemWindowInsetTop() | 物理像素 px(Android 所有 insets 都是 px) |
| WebView 渲染 | 1 CSS px = 1 dp(默认 devicePixelRatio = density) |
| native 布局 margin | px(native 里直接当 px 用是对的) |
所以:桥把 native 的 px 原样返回 → 前端写成 <px>px → WebView 里就是 px 个 dp → 顶部让位被放大 density 倍。例:420dpi(density 2.625)真机状态栏 24dp=63px,前端拿到 63px = 63dp ≈ 165 物理像素,顶部让出 2.6 倍状态栏。
同一条链路里,底部导航栏/IME 走的是 native bottomMargin(px),不经过 CSS,所以底部没这个问题——两条链路单位语义不同,最容易踩的就是「以为顶部底部一样」。
解决
桥返回前做 px → dp 换算,前端照旧当 CSS px 用:
@JavascriptInterface
public String getStatusBarHeight() {
int px = MainActivity.cachedTopInset();
int dp = Math.round(px / webView.getResources().getDisplayMetrics().density);
return String.valueOf(dp);
}只改 native、重打 APK 即可,前端不用发版(前端契约本来就是「这个数 = CSS px」)。反过来让前端除以 devicePixelRatio 也行,但会和 native 换算互斥——只能一端换算,别两端都除。
下次怎么识别
- 症状「web 正常 / APK 异常」且与状态栏相关 → 先怀疑桥返回值的单位;
- gap 大小随设备密度变化(低密度模拟器不出现)→ 单位换算问题实锤;
- native 里的值给了 CSS,就要想一遍「物理 px / dp / CSS px」的换算链。
相关概念
- 密度无关像素(density-independent pixels, dp)
- 设备像素比(devicePixelRatio)
- WindowInsets
- Edge-to-Edge 沉浸式布局(setDecorFitsSystemWindows)
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Programming