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:
@@ -1,81 +1,17 @@
|
||||
package dev.ncode.sp404layers;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
/**
|
||||
* Android 15+ forces edge-to-edge for apps targeting SDK 35, and Android 16
|
||||
* removes the windowOptOutEdgeToEdgeEnforcement escape hatch entirely at
|
||||
* targetSdk 36 (which is Capacitor 8's default). An Android WebView does not
|
||||
* populate env(safe-area-inset-*), so the web layer has no way to discover the
|
||||
* system bar geometry on its own.
|
||||
* Deliberately empty.
|
||||
*
|
||||
* This bridges the gap: the app draws behind the bars, and the real insets are
|
||||
* pushed into CSS custom properties that native.css consumes.
|
||||
* An earlier version installed its own OnApplyWindowInsetsListener on the
|
||||
* WebView and pushed the insets into CSS custom properties. That was written
|
||||
* before I found that Capacitor 8 ships a SystemBars plugin which already does
|
||||
* this, from a listener on the decor view.
|
||||
*
|
||||
* The two stacked rather than replaced each other, and the app ended up drawing
|
||||
* under the system bars at random. See the "Device notes" section of CLAUDE.md.
|
||||
* Bar styling and inset handling are now configured in capacitor.config.json.
|
||||
*/
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Draw behind the system bars.
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
// The UI is dark, so the status/nav bar icons must be light.
|
||||
WindowInsetsControllerCompat controller =
|
||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
if (controller != null) {
|
||||
controller.setAppearanceLightStatusBars(false);
|
||||
controller.setAppearanceLightNavigationBars(false);
|
||||
}
|
||||
|
||||
final View webView = getBridge().getWebView();
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(webView, (v, windowInsets) -> {
|
||||
Insets bars = windowInsets.getInsets(
|
||||
WindowInsetsCompat.Type.systemBars()
|
||||
| WindowInsetsCompat.Type.displayCutout());
|
||||
|
||||
// CSS pixels are density-independent; WindowInsets are physical px.
|
||||
float density = getResources().getDisplayMetrics().density;
|
||||
final int top = Math.round(bars.top / density);
|
||||
final int right = Math.round(bars.right / density);
|
||||
final int bottom = Math.round(bars.bottom / density);
|
||||
final int left = Math.round(bars.left / density);
|
||||
|
||||
// Returns "1" once applied, "0" while native.js has not loaded yet.
|
||||
final String js = String.format(
|
||||
java.util.Locale.US,
|
||||
"(window.__applyInsets?(window.__applyInsets(%d,%d,%d,%d),1):0)",
|
||||
top, right, bottom, left);
|
||||
|
||||
// Insets usually arrive before the page has run any JS, so retry on
|
||||
// a bounded schedule instead of firing once into the void.
|
||||
applyWithRetry(js, 0);
|
||||
|
||||
// Do not consume: other listeners (and the IME) may still need these.
|
||||
return windowInsets;
|
||||
});
|
||||
}
|
||||
|
||||
private static final int INSET_RETRY_LIMIT = 25; // ~5s at 200ms
|
||||
private static final long INSET_RETRY_DELAY_MS = 200L;
|
||||
|
||||
/** Pushes the inset JS, retrying until native.js has defined __applyInsets. */
|
||||
private void applyWithRetry(final String js, final int attempt) {
|
||||
final View webView = getBridge().getWebView();
|
||||
webView.post(() -> getBridge().getWebView().evaluateJavascript(js, value -> {
|
||||
boolean applied = value != null && value.contains("1");
|
||||
if (!applied && attempt < INSET_RETRY_LIMIT) {
|
||||
webView.postDelayed(() -> applyWithRetry(js, attempt + 1), INSET_RETRY_DELAY_MS);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
|
||||
Reference in New Issue
Block a user