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 =