diff --git a/android/capacitor/src/main/assets/native-bridge.js b/android/capacitor/src/main/assets/native-bridge.js
index f5e7cc440..20a21b4fb 100644
--- a/android/capacitor/src/main/assets/native-bridge.js
+++ b/android/capacitor/src/main/assets/native-bridge.js
@@ -367,12 +367,6 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
index b3c95916a..8b9de8849 100644
--- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
+++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java
@@ -4,11 +4,9 @@
import android.content.pm.PackageInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.os.Build;
import android.util.TypedValue;
import android.view.View;
import android.view.Window;
-import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
@@ -33,9 +31,9 @@ public class SystemBars extends Plugin {
static final String BAR_STATUS_BAR = "StatusBar";
static final String BAR_GESTURE_BAR = "NavigationBar";
- // TODO: In Cap 9, add an additional option "full"
static final String INSETS_HANDLING_CSS = "css";
static final String INSETS_HANDLING_DISABLE = "disable";
+ static final String INSETS_HANDLING_NATIVE = "native";
// https://issues.chromium.org/issues/40699457
private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
@@ -61,9 +59,22 @@ function capacitorSystemBarsCheckMetaViewport() {
private String currentStatusBarStyle = STYLE_DEFAULT;
private String currentGestureBarStyle = STYLE_DEFAULT;
+ // Declare variable at this scope to help prevent adding multiple listeners.
+ private WebViewListener webViewListener;
+
+ private void warnAboutUnsupportedConfigurationValues() {
+ boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getBoolean("resizeOnFullScreen", false);
+
+ if (!INSETS_HANDLING_DISABLE.equals(insetsHandling) && keyboardResizeOnFullScreen) {
+ Logger.warn(
+ "SystemBars",
+ "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
+ );
+ }
+ }
+
@Override
public void load() {
- getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
super.load();
initSystemBars();
@@ -73,15 +84,25 @@ public void load() {
protected void handleOnStart() {
super.handleOnStart();
- this.getBridge().addWebViewListener(
- new WebViewListener() {
+ if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
+ return;
+ }
+
+ if (webViewListener == null) {
+ webViewListener = new WebViewListener() {
@Override
public void onPageCommitVisible(WebView view, String url) {
super.onPageCommitVisible(view, url);
- getBridge().getWebView().requestApplyInsets();
+ bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
+ hasViewportCover = res.equals("true");
+
+ // Request new execution tree of `setOnApplyWindowInsetsListener`
+ bridge.getWebView().requestApplyInsets();
+ });
}
- }
- );
+ };
+ this.getBridge().addWebViewListener(webViewListener);
+ }
}
@Override
@@ -93,11 +114,20 @@ protected void handleOnConfigurationChanged(Configuration newConfig) {
}
private void initSystemBars() {
+ // If you already know what the value of the `viewport-fit=` meta tag is going to be,
+ // passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
+ String configuredInitialViewportFitValueHint = getConfig().getString("initialViewportFitValueHint", "");
+ hasViewportCover = "cover".equals(configuredInitialViewportFitValueHint);
+
String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
boolean hidden = getConfig().getBoolean("hidden", false);
String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
- if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) {
+ if (
+ INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
+ INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
+ INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)
+ ) {
insetsHandling = configuredInsetsHandling;
} else {
Logger.warn(
@@ -107,8 +137,9 @@ private void initSystemBars() {
insetsHandling = INSETS_HANDLING_CSS;
}
+ warnAboutUnsupportedConfigurationValues();
+
initWindowInsetsListener();
- initSafeAreaCSSVariables();
getBridge().executeOnMainThread(() -> {
setStyle(style, "");
@@ -152,51 +183,14 @@ public void setAnimation(final PluginCall call) {
call.resolve();
}
- @JavascriptInterface
- public void onDOMReady() {
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
- getActivity().runOnUiThread(() -> {
- this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
- hasViewportCover = res.equals("true");
-
- getBridge().getWebView().requestApplyInsets();
- });
- });
- }
- }
-
- private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
- Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
- if (insets.isVisible(WindowInsetsCompat.Type.ime())) {
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0);
- }
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom);
- }
-
- private void initSafeAreaCSSVariables() {
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
- WindowInsetsCompat insets;
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- View v = (View) this.getBridge().getWebView().getParent();
- insets = ViewCompat.getRootWindowInsets(v);
- } else {
- insets = WindowInsetsCompat.CONSUMED;
- }
-
- if (insets != null) {
- Insets safeAreaInsets = calcSafeAreaInsets(insets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
- }
- }
- }
-
private void initWindowInsetsListener() {
if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
return;
}
- ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
+ View view = getActivity().getWindow().getDecorView();
+
+ ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;
Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
@@ -207,10 +201,7 @@ private void initWindowInsetsListener() {
// We need to correct for a possible shown IME
v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
- Insets safeAreaInsets = calcSafeAreaInsets(insets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
-
- return new WindowInsetsCompat.Builder(insets)
+ WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
.setInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
Insets.of(
@@ -221,18 +212,20 @@ private void initWindowInsetsListener() {
)
)
.build();
- }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- // We need to correct for a possible shown IME
- v.setPadding(
- systemBarsInsets.left,
- systemBarsInsets.top,
- systemBarsInsets.right,
- keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
- );
+ injectSafeAreaCSS(newInsets);
+
+ return newInsets;
}
+ // We need to correct for a possible shown IME
+ v.setPadding(
+ systemBarsInsets.left,
+ systemBarsInsets.top,
+ systemBarsInsets.right,
+ keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
+ );
+
// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
// See: https://issues.chromium.org/issues/461332423
@@ -240,20 +233,28 @@ private void initWindowInsetsListener() {
.setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
.build();
- Insets safeAreaInsets = calcSafeAreaInsets(newInsets);
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
+ injectSafeAreaCSS(newInsets);
return newInsets;
});
}
- private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
+ private void injectSafeAreaCSS(WindowInsetsCompat insets) {
+ if (!INSETS_HANDLING_CSS.equals(insetsHandling)) {
+ return;
+ }
+
+ Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
+ boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
+
// Convert pixels to density-independent pixels
float density = getActivity().getResources().getDisplayMetrics().density;
- float topPx = top / density;
- float rightPx = right / density;
- float bottomPx = bottom / density;
- float leftPx = left / density;
+ float topPx = systemBarsInsets.top / density;
+ float rightPx = systemBarsInsets.right / density;
+ // For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
+ // but for these injected values we have to handle that manually (for all WebView versions).
+ float bottomPx = (keyboardVisible ? 0 : systemBarsInsets.bottom) / density;
+ float leftPx = systemBarsInsets.left / density;
// Execute JavaScript to inject the CSS
getBridge().executeOnMainThread(() -> {
diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts
index 1103b6585..d36e23f48 100644
--- a/cli/src/declarations.ts
+++ b/cli/src/declarations.ts
@@ -772,19 +772,39 @@ export interface PluginsConfig {
*
* This option is only supported on Android.
*
- * `css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
+ * `native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
*
- * `disable` = Disable CSS variables injection.
+ * `css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
+ *
+ * `disable` = (not recommended) Disable safe area insets handling completely.
+ * This shifts the responsibility from Capacitor to your own code to handle the insets.
+ * Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
*
* @default "css"
*/
- insetsHandling?: 'css' | 'disable';
+ insetsHandling?: 'native' | 'css' | 'disable';
+
+ /**
+ * Set an initial value for the to be detected `viewport-fit=` meta tag value.
+ * For most apps that support edge-to-edge this value will eventually be `cover`.
+ * Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
+ * If you know the value to be `cover` initially, you can set it here.
+ * The value will always end up correctly, no matter what you set here,
+ * as long as `insetsHandling` is set to `native` or `css`.
+ * It only exists to help prevent layout jumps and glitches.
+ *
+ * This option is only supported on Android.
+ *
+ * @default undefined
+ */
+ initialViewportFitValueHint?: 'auto' | 'contain' | 'cover';
+
/**
* The style of the text and icons of the system bars.
*
* This option is only supported on Android.
*
- * @default `DEFAULT`
+ * @default 'css'
*/
style?: string;
diff --git a/core/native-bridge.ts b/core/native-bridge.ts
index 4fc028f37..50f799f81 100644
--- a/core/native-bridge.ts
+++ b/core/native-bridge.ts
@@ -377,13 +377,6 @@ const initBridge = (w: any): void => {
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
-
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =
diff --git a/core/system-bars.md b/core/system-bars.md
index 67275077a..fb6077f5c 100644
--- a/core/system-bars.md
+++ b/core/system-bars.md
@@ -24,17 +24,7 @@ The status bar visibility defaults to visible and the style defaults to
## Android Note
-Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin will inject the correct inset values into a new CSS variable(s) named `--safe-area-inset-x` that you can use as a fallback in your frontend styles:
-
-```css
-html {
- padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
- padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
- padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
- padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
-}
-```
-To control this behavior, use the `insetsHandling` configuration setting.
+Due to a [bug](https://issues.chromium.org/issues/40699457) in some older versions of Android WebView (< 140), correct safe area values are not available via the `safe-area-inset-x` CSS `env` variables. This plugin has two ways to workaround this. To control this behavior, use the `insetsHandling` configuration setting.
## Example
@@ -73,7 +63,8 @@ const setStatusBarAnimation = async () => {
## Configuration
| Prop | Type | Description | Default |
| ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ |
-| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android. This option is only supported on Android.
`css` = Injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = Disable CSS variables injection. | css |
+| **`insetsHandling`** | string | Specifies how to handle problematic insets on Android.
This option is only supported on Android.
`native` = (recommended) For older Chromium versions (< v140) this embeds the webview with padding and sets the `env(safe-area-inset-*)` variables to `0px`. For newer Chromium versions (>= v140) this makes sure the webview adheres to the `viewport-fit` meta tag. If set to `viewport-fit="cover"` this will make the webview edge-to-edge and the `env(safe-area-inset-*)` variables will contain the correct values. With those values you could set padding for example so make sure the webview is shown correctly.
`css` = This is the same as `native`, but it also injects CSS variables (`--safe-area-inset-*`) containing correct safe area inset values into the webview.
`disable` = (not recommended) Disable safe area insets handling completely.
This shifts the responsibility from Capacitor to your own code to handle the insets.
Be aware that this might result in a visually broken UI if your native app code and the content loaded into the webview do not correctly handle safe area insets.
| css |
+| **`initialViewportFitValueHint`** | string | Set an initial value for the to be detected `viewport-fit=` meta tag value.
For most apps that support edge-to-edge this value will eventually be `cover`.
Therefore you might want to set this value to `cover` to help prevent layout jumps and glitches.
If you know the value to be `cover` initially, you can set it here.
The value will always end up correctly, no matter what you set here,
as long as `insetsHandling` is set to `native` or `css`.
It only exists to help prevent layout jumps and glitches.
This option is only supported on Android. | false |
| **`style`** | string | The style of the text and icons of the system bars. | DEFAULT |
| **`hidden`** | boolean | Hide the system bars on start. | false |
| **`animation`** | string | The type of status bar animation used when showing or hiding. This option is only supported on iOS. | FADE |
diff --git a/ios/Capacitor/Capacitor/assets/native-bridge.js b/ios/Capacitor/Capacitor/assets/native-bridge.js
index f5e7cc440..20a21b4fb 100644
--- a/ios/Capacitor/Capacitor/assets/native-bridge.js
+++ b/ios/Capacitor/Capacitor/assets/native-bridge.js
@@ -367,12 +367,6 @@ var nativeBridge = (function (exports) {
}
};
const platform = getPlatformId(win);
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
- // add DOM ready listener for System Bars
- document.addEventListener('DOMContentLoaded', function () {
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
- });
- }
if (platform == 'android' || platform == 'ios') {
// patch document.cookie on Android/iOS
win.CapacitorCookiesDescriptor =