跪拜 Guibai
← All articles
Android

Android 16 Shrinks WebView Viewports by 40dp; a Two-Layer Fix Stops the Gap

By 用户42381622907 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Any WebView-based app targeting Android 15+ can silently lose 40dp or more of usable screen space at the top, even with immersive mode and `viewport-fit=cover` in place. The decor-root inset consumption pattern is the only reliable native fix, and the JS fallback catches devices where the native fix doesn't take.

Summary

On a punch-hole device running Android 16, a Capacitor game's WebView viewport measured 360×752 instead of the full 360×792. The missing 40dp matched the status bar and cutout height exactly. Standard fixes — `viewport-fit=cover`, `FLAG_LAYOUT_NO_LIMITS`, and WebView-level inset interception — all failed because Chrome reads window root insets directly, bypassing the dispatch chain. A red-blue background experiment confirmed the WebView itself filled the screen; Chrome was simply refusing to use the top 40dp for layout. The root cause is Android's edge-to-edge enforcement, which feeds non-zero system bar insets to the WebView even when those bars are hidden. The fix consumes system window insets at the decor root node so Chrome sees zero insets, paired with a JS fallback that detects any remaining offset and shifts fixed full-screen layers upward. A reusable diagnostic methodology emerged: print viewport metrics in a fixed overlay, confirm devicePixelRatio before converting physical pixels, and use contrasting background colors to identify which layer is actually visible in the gap.

Takeaways
Android 15/16's mandatory edge-to-edge enforcement feeds non-zero system bar insets to WebView even when status bars are hidden, shrinking the CSS viewport.
`viewport-fit=cover` does not reliably work in Chromium WebView on affected Android versions.
Chrome reads window root insets via `getRootWindowInsets`, so WebView-level `setOnApplyWindowInsetsListener` calls have no effect on the viewport.
Consuming system window insets at the decor root node (`getWindow().getDecorView()`) makes Chrome see zero insets and restores the full viewport.
A JS fallback that computes `screen.height - window.innerHeight` and shifts fixed full-screen layers upward catches devices where the native fix fails.
Setting WebView and window backgrounds to contrasting colors instantly reveals whether the gap is a view offset or a viewport shrink.
Always confirm `devicePixelRatio` before converting physical pixels to dp; a dpr mismatch can send debugging in the wrong direction.
Incrementing `versionName`/`versionCode` on every build prevents confusion about whether a fix was actually installed.
Conclusions

Chrome's direct read of root insets, bypassing the dispatch chain, is a deliberate architectural choice that makes WebView inset handling fundamentally different from standard Android views — and it's poorly documented.

The `viewport-fit=cover` meta tag has been unreliable in Android WebView for years, yet it remains the first thing developers reach for; the ecosystem needs a clearer deprecation or fix path.

A diagnostic overlay that prints viewport metrics in a `position:fixed` element is a low-effort, high-signal debugging technique that more WebView developers should adopt as a first step.

The dual-layer fix — native root consumption plus JS offset compensation — acknowledges that WebView behavior varies across OEMs and Chromium versions, and that a single fix point is brittle.

Concepts & terms
Window root insets
The system-provided inset values (status bar, navigation bar, cutout) attached to the window's root view. Chrome's WebView reads these directly via `getRootWindowInsets()` rather than through the standard view dispatch chain, making WebView-level inset listeners ineffective for viewport control.
Decor view
The top-level view in an Android window hierarchy that contains the window decorations and content. Attaching an `OnApplyWindowInsetsListener` to `getWindow().getDecorView()` intercepts insets before any child view — including WebView — receives them.
Edge-to-edge enforcement
Starting in Android 15, apps targeting SDK 35+ have edge-to-edge display forced by the platform. The system draws content behind system bars by default and provides inset values so apps can avoid overlaps, but WebView's handling of these insets differs from native views.
From the discussion
Featured comments
向新出发叭

This debugging process is textbook-level 👏 I fell into the exact same pit last year working on an H5 container project: after Android 15/16 forced edge-to-edge, the WebView viewport was shrunk by the window root insets. I also tried attaching inset listeners on the WebView, consuming them, and feeding them back—went through the whole cycle to no avail. The deepest lesson is exactly what the article says—Chrome reads the root window insets and doesn't go through the dispatch chain, so intercepting at the child View layer is futile. Two small tricks I've used myself: 1) Before fixing, use Layout Inspector or adb shell dumpsys window to confirm the source of the insets, to avoid repeatedly misdiagnosing it as a punch-hole config issue; 2) The trick of consuming insets at the decor root node works great for full-screen game pages, but if the Activity has a custom top bar that relies on systemBars padding, it'll get hit too—recommend enabling it only on full-screen pages. Also, the lesson about 'confirm dpr before converting' really hits home, I've made that mistake too haha. Thanks for sharing, bookmarked!

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗