Skip to content

Commit 090acee

Browse files
committed
fix(android): keep backgrounds behind the system bars
Padding the WebView kept the page clear of the status bar but replaced the page background there with a flat bar. Draw edge to edge again with transparent system bars and give the page the real insets instead: MainActivity sets --safe-area-inset-* after every insets change and page load, and only the keyboard shrinks the WebView. The status bar icons now follow the app theme (light icons in the always-dark scanner).
1 parent 788d11f commit 090acee

4 files changed

Lines changed: 86 additions & 21 deletions

File tree

‎android/app/src/main/java/dev/cfb/manavault/MainActivity.java‎

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import android.util.Log;
1010
import android.view.View;
1111
import android.webkit.CookieManager;
12+
import android.webkit.WebView;
13+
14+
import java.util.Locale;
1215

1316
import androidx.core.graphics.Insets;
1417
import androidx.core.view.ViewCompat;
@@ -19,6 +22,7 @@
1922
import com.getcapacitor.CapConfig;
2023
import com.getcapacitor.BridgeActivity;
2124
import com.getcapacitor.Plugin;
25+
import com.getcapacitor.WebViewListener;
2226
import com.getcapacitor.annotation.CapacitorPlugin;
2327

2428
public 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();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Capacitor, SystemBars, SystemBarsStyle } from "@capacitor/core"
2+
3+
/**
4+
* The native shells draw the page behind transparent system bars, so their icons must contrast
5+
* with the page: light icons on a dark page, dark icons on a light one.
6+
*/
7+
export function setNativeSystemBarsTheme(theme: "light" | "dark") {
8+
if (!Capacitor.isNativePlatform()) return
9+
SystemBars.setStyle({
10+
style: theme === "dark" ? SystemBarsStyle.Dark : SystemBarsStyle.Light,
11+
}).catch(() => {
12+
// Older native shells without the SystemBars plugin keep their fixed style.
13+
})
14+
}
15+
16+
/** The system bar style for the page's current theme. */
17+
export function restoreNativeSystemBarsTheme() {
18+
setNativeSystemBarsTheme(document.documentElement.dataset.theme === "light" ? "light" : "dark")
19+
}

‎assets/react/src/lib/theme.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useMutation } from "@apollo/client/react"
2+
import { setNativeSystemBarsTheme } from "./native-system-bars"
23
import {
34
createContext,
45
type Dispatch,
@@ -136,6 +137,8 @@ export function ThemeProvider({ children }: { children: ReactNode }) {
136137
setResolvedTheme(resolveTheme(theme))
137138
}, [theme])
138139

140+
useEffect(() => setNativeSystemBarsTheme(resolvedTheme), [resolvedTheme])
141+
139142
useEffect(() => {
140143
document.documentElement.setAttribute("data-theme-style", themeStyle)
141144
if (!account) writeStorage(styleStorageKey, themeStyle === "glass" ? null : themeStyle)

‎assets/react/src/pages/scan/scan-page.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { Link } from "@tanstack/react-router"
22
import { CameraOff, List, LoaderCircle, Settings2, X } from "lucide-react"
33
import { useEffect, useState } from "react"
4+
import {
5+
restoreNativeSystemBarsTheme,
6+
setNativeSystemBarsTheme,
7+
} from "../../lib/native-system-bars"
48
import { Button } from "../../components/ui/button"
59
import { PrintingSheet } from "./printing-sheet"
610
import type { RecognizerState } from "./recognition/use-recognizer"
@@ -38,6 +42,12 @@ export function ScanPage() {
3842
return stop
3943
}, [start, stop])
4044

45+
// The scanner is always dark, whatever the app theme: light status bar icons while open.
46+
useEffect(() => {
47+
setNativeSystemBarsTheme("dark")
48+
return restoreNativeSystemBarsTheme
49+
}, [])
50+
4151
// Browsers keep audio locked until a user gesture; the first tap anywhere unlocks it.
4252
useEffect(() => {
4353
window.addEventListener("pointerdown", unlockScanSounds, { once: true })

0 commit comments

Comments
 (0)