Render between the system bars deterministically; delete my inset bridge

The app sometimes drew under the status and gesture bars and sometimes
between them. Cause: Capacitor 8 ships a SystemBars plugin that already
handles insets, from a listener on the decor view, and my MainActivity
had installed another one on the WebView. Different views, so they
stacked rather than replaced each other and the result depended on which
fired last.

SystemBars switches on the viewport meta tag. With viewport-fit=cover it
passes insets through once onPageCommitVisible has checked the tag, decor
padding drops to 0 and the WebView grows 735 -> 820, leaving the page to
place itself from injected CSS vars. Without it the plugin pads the decor
view and the WebView is laid out between the bars already.

So sync-www.sh now strips viewport-fit=cover from the generated www/
(patch 5), MainActivity is an empty BridgeActivity, native.css and
native.js no longer touch insets, and bar style moves to
capacitor.config.json. No native->web handshake remains, so there is
nothing to race and no launch-time layout jump.

Verified identical across 3 cold launches, 2 resumes and an in-place
reload: h=735 padTop=8px overflowBy=0, panel 485px, pad rows 44.8px.

Costs no space: 820-45-114 and 735-8-66 both leave 661px. The bars take
the same room either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 23:26:31 +03:00
parent 5f9d89d9fb
commit 066b9e8c34
6 changed files with 83 additions and 128 deletions

View File

@@ -117,16 +117,37 @@ code about to be rewritten in React anyway.
Explore → go to Explore; otherwise exit. Registering a listener suppresses
Capacitor's default history navigation.
**Edge-to-edge is handled natively, not avoided.** Android 16 is API 36 and
Capacitor 8 targets SDK 36, which is exactly the combination where
`windowOptOutEdgeToEdgeEnforcement` stops working. An Android WebView never
populates `env(safe-area-inset-*)`, so `MainActivity` installs an
`OnApplyWindowInsetsListener`, converts px → dp, and pushes the values into
`--ins-t/r/b/l` via `window.__applyInsets()`. `native.css` re-points the
reference's `--sab` at `--ins-b`, so `.bnav` and `.sheet` need no changes.
**System bars: let Capacitor do it, by dropping `viewport-fit=cover`.** The app
renders between the status and gesture bars, always.
The push retries on a bounded schedule (25 × 200ms) because insets typically
arrive before the page has run any JS.
Capacitor 8 ships a `SystemBars` plugin that owns inset handling from a listener
on the **decor view**. Its behaviour hinges on the viewport meta tag:
- **with `viewport-fit=cover`** (what `reference/` has), `shouldPassthroughInsets`
becomes true once `onPageCommitVisible` has checked the tag and called
`requestApplyInsets()`. Decor padding drops to 0, the WebView grows from 735 to
820, and the page must place itself from injected CSS variables.
- **without it**, the plugin pads the decor view instead. The WebView is 735px
and already sits between the bars; `env(safe-area-inset-*)` and the
reference's `--sab` are 0, and `.bnav` and `.sheet` are correct as written.
So `sync-www.sh` strips `viewport-fit=cover` from the generated `www/` (patch 5).
No native→web handshake exists, so there is nothing to race and no launch-time
layout jump. Verified identical across cold launch, resume and in-place reload:
`h=735 padTop=8px overflowBy=0`.
**This costs no space.** 820 − 45 − 114 and 735 − 8 − 66 both leave 661px, because
the bars take the same room whether the WebView is padded or draws underneath.
Reclaiming that 85px would mean *hiding* the bars, which is a different choice.
An earlier version did the opposite: `MainActivity` installed its own
`OnApplyWindowInsetsListener` on the WebView and pushed `--ins-t/r/b/l` through
`window.__applyInsets()`, with a 25 × 200ms retry. That was written without
realising Capacitor already had a plugin for this. The two listeners were on
different views, so they stacked rather than replaced each other, and the app
drew under the bars at random depending on which fired last. `MainActivity` is
now empty and bar styling is configured in `capacitor.config.json`
(`SystemBars.style: DARK`).
**No community plugins.** Only first-party: `@capacitor/core`, `/cli`,
`/android`, `/haptics`, `/preferences`, `/app`.
@@ -176,9 +197,14 @@ function rewrites `#strip` wholesale.
`cap sync` does not regenerate these — keep them when recreating `android/`:
- `android/app/src/main/java/dev/ncode/sp404layers/MainActivity.java` — inset bridge
- `android/app/src/main/AndroidManifest.xml` — `android:screenOrientation="portrait"`
That is the only one. `MainActivity.java` is deliberately an empty
`BridgeActivity`: everything it used to do is either configured in
`capacitor.config.json` or handled by Capacitor's own `SystemBars` plugin. If
you find yourself adding an inset listener there, read the system-bars decision
above first — that path has already been tried and reverted.
## Device notes (TCL 50 PRO NXTPAPER 5G, Android 16 / API 36)
**adb needs a udev rule.** The phone is USB `1bbb:0c01`. Ubuntu's