99import android .util .Log ;
1010import android .view .View ;
1111import android .webkit .CookieManager ;
12+ import android .webkit .WebView ;
13+
14+ import java .util .Locale ;
1215
1316import androidx .core .graphics .Insets ;
1417import androidx .core .view .ViewCompat ;
1922import com .getcapacitor .CapConfig ;
2023import com .getcapacitor .BridgeActivity ;
2124import com .getcapacitor .Plugin ;
25+ import com .getcapacitor .WebViewListener ;
2226import com .getcapacitor .annotation .CapacitorPlugin ;
2327
2428public class MainActivity extends BridgeActivity {
@@ -27,6 +31,9 @@ public class MainActivity extends BridgeActivity {
2731 private static final String PREFERENCES_NAME = "NativeShell" ;
2832 private static final String SERVER_URL_KEY = "serverUrl" ;
2933
34+ /** System bar and cutout insets in px; the page receives them as CSS variables. */
35+ private Insets safeArea = Insets .NONE ;
36+
3037
3138 @ Override
3239 protected void onCreate (Bundle savedInstanceState ) {
@@ -39,45 +46,71 @@ protected void onCreate(Bundle savedInstanceState) {
3946 registerPlugin (InAppHttpNavigationPlugin .class );
4047 registerPlugin (SharedImportPlugin .class );
4148 registerPlugin (NativeShellPlugin .class );
42- WindowCompat .setDecorFitsSystemWindows (getWindow (), true );
49+ // Edge to edge on every Android version (Android 15+ enforces it anyway): page
50+ // backgrounds run behind transparent system bars and the page pads its own content.
51+ WindowCompat .setDecorFitsSystemWindows (getWindow (), false );
4352 super .onCreate (savedInstanceState );
4453
45- getWindow ().setStatusBarColor (APP_CHROME_COLOR );
46- getWindow ().setNavigationBarColor (APP_CHROME_COLOR );
54+ getWindow ().setStatusBarColor (Color . TRANSPARENT );
55+ getWindow ().setNavigationBarColor (Color . TRANSPARENT );
4756
4857 WindowInsetsControllerCompat controller = WindowCompat .getInsetsController (getWindow (), getWindow ().getDecorView ());
4958 controller .setAppearanceLightStatusBars (false );
5059 controller .setAppearanceLightNavigationBars (false );
5160
52- keepWebViewClearOfSystemBars ();
61+ passSafeAreaToPage ();
5362 }
5463
5564 /**
56- * Android 15+ draws apps edge to edge and ignores the status/navigation bar colors, so the
57- * WebView would sit under the system bars. Rather than rely on the page reading the insets
58- * ( Capacitor's SystemBars CSS handling is disabled in capacitor.config.json because some
59- * WebViews reported them as 0), pad the WebView's container by the bar and cutout insets
60- * and paint the padding in the app chrome color. The on-screen keyboard shrinks the bottom .
65+ * The page draws under the system bars and reads their size from --safe-area-inset-*
66+ * (assets/css/app.css takes the larger of that and env()), which this sets after every
67+ * insets change and page load. Capacitor's own SystemBars CSS handling is disabled in
68+ * capacitor.config.json because it left those variables at 0 on some devices. Only the
69+ * on-screen keyboard shrinks the WebView .
6170 */
62- private void keepWebViewClearOfSystemBars () {
63- View container = (View ) getBridge ().getWebView ().getParent ();
71+ private void passSafeAreaToPage () {
72+ WebView webView = getBridge ().getWebView ();
73+ View container = (View ) webView .getParent ();
6474 container .setBackgroundColor (APP_CHROME_COLOR );
6575 ViewCompat .setOnApplyWindowInsetsListener (container , (view , insets ) -> {
66- int types = WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout ();
67- Insets bars = insets .getInsets (types );
68- Insets ime = insets .getInsets (WindowInsetsCompat .Type .ime ());
76+ Insets bars = insets .getInsets (WindowInsetsCompat .Type .systemBars () | WindowInsetsCompat .Type .displayCutout ());
6977 boolean keyboard = insets .isVisible (WindowInsetsCompat .Type .ime ());
70- view .setPadding (bars .left , bars .top , bars .right , keyboard ? Math .max (ime .bottom , bars .bottom ) : bars .bottom );
71- // Hand the WebView zero insets (not CONSUMED, which stops later recalculation), so
72- // env(safe-area-inset-*) is 0 inside the already padded area.
73- return new WindowInsetsCompat .Builder (insets )
74- .setInsets (types , Insets .NONE )
75- .setInsets (WindowInsetsCompat .Type .ime (), Insets .NONE )
76- .build ();
78+ int keyboardHeight = insets .getInsets (WindowInsetsCompat .Type .ime ()).bottom ;
79+ view .setPadding (0 , 0 , 0 , keyboard ? keyboardHeight : 0 );
80+ safeArea = Insets .of (bars .left , bars .top , bars .right , keyboard ? 0 : bars .bottom );
81+ injectSafeArea (webView );
82+ return insets ;
83+ });
84+ getBridge ().addWebViewListener (new WebViewListener () {
85+ @ Override
86+ public void onPageCommitVisible (WebView view , String url ) {
87+ injectSafeArea (view );
88+ }
89+
90+ @ Override
91+ public void onPageLoaded (WebView view ) {
92+ injectSafeArea (view );
93+ }
7794 });
7895 ViewCompat .requestApplyInsets (container );
7996 }
8097
98+ private void injectSafeArea (WebView webView ) {
99+ float density = getResources ().getDisplayMetrics ().density ;
100+ String script = String .format (
101+ Locale .US ,
102+ "(function(s){s.setProperty('--safe-area-inset-top','%.1fpx');"
103+ + "s.setProperty('--safe-area-inset-right','%.1fpx');"
104+ + "s.setProperty('--safe-area-inset-bottom','%.1fpx');"
105+ + "s.setProperty('--safe-area-inset-left','%.1fpx');})"
106+ + "(document.documentElement.style)" ,
107+ safeArea .top / density ,
108+ safeArea .right / density ,
109+ safeArea .bottom / density ,
110+ safeArea .left / density );
111+ webView .post (() -> webView .evaluateJavascript (script , null ));
112+ }
113+
81114 @ Override
82115 public void onPause () {
83116 flushWebViewCookies ();
0 commit comments