From fef531668b19834b9e580ac85f884afd50b5a9a9 Mon Sep 17 00:00:00 2001 From: Piotr Andreassen Blasiak <2700143+piotrblasiak@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:23:09 +0200 Subject: [PATCH] feat(SystemBars): add keyboardInsetsHandling option to let the keyboard overlay the webview --- .../com/getcapacitor/plugin/SystemBars.java | 46 ++++++++++++++++--- cli/src/declarations.ts | 15 ++++++ core/system-bars.md | 1 + 3 files changed, 55 insertions(+), 7 deletions(-) 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 8b9de8849..6833100c0 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -35,6 +35,9 @@ public class SystemBars extends Plugin { static final String INSETS_HANDLING_DISABLE = "disable"; static final String INSETS_HANDLING_NATIVE = "native"; + static final String KEYBOARD_INSETS_HANDLING_RESIZE = "resize"; + static final String KEYBOARD_INSETS_HANDLING_NONE = "none"; + // https://issues.chromium.org/issues/40699457 private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140; // https://issues.chromium.org/issues/457682720 @@ -54,6 +57,7 @@ function capacitorSystemBarsCheckMetaViewport() { """; private String insetsHandling = INSETS_HANDLING_CSS; + private boolean resizeForKeyboard = true; private boolean hasViewportCover = false; private String currentStatusBarStyle = STYLE_DEFAULT; @@ -137,6 +141,20 @@ private void initSystemBars() { insetsHandling = INSETS_HANDLING_CSS; } + String configuredKeyboardInsetsHandling = getConfig().getString("keyboardInsetsHandling", KEYBOARD_INSETS_HANDLING_RESIZE); + if (KEYBOARD_INSETS_HANDLING_NONE.equals(configuredKeyboardInsetsHandling)) { + resizeForKeyboard = false; + } else if (!KEYBOARD_INSETS_HANDLING_RESIZE.equals(configuredKeyboardInsetsHandling)) { + Logger.warn( + "SystemBars", + "Unknown keyboardInsetsHandling value '" + + configuredKeyboardInsetsHandling + + "'. Falling back to '" + + KEYBOARD_INSETS_HANDLING_RESIZE + + "'." + ); + } + warnAboutUnsupportedConfigurationValues(); initWindowInsetsListener(); @@ -195,14 +213,14 @@ private void initWindowInsetsListener() { Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); - boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + boolean keyboardVisible = resizeForKeyboard && insets.isVisible(WindowInsetsCompat.Type.ime()); if (shouldPassthroughInsets) { // We need to correct for a possible shown IME v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0); - WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets) - .setInsets( + WindowInsetsCompat newInsets = withKeyboardInsetsHandling( + new WindowInsetsCompat.Builder(insets).setInsets( WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of( systemBarsInsets.left, @@ -211,7 +229,7 @@ private void initWindowInsetsListener() { getBottomInset(systemBarsInsets, keyboardVisible) ) ) - .build(); + ).build(); injectSafeAreaCSS(newInsets); @@ -229,9 +247,12 @@ private void initWindowInsetsListener() { // 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 - WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets) - .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0)) - .build(); + WindowInsetsCompat newInsets = withKeyboardInsetsHandling( + new WindowInsetsCompat.Builder(insets).setInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), + Insets.of(0, 0, 0, 0) + ) + ).build(); injectSafeAreaCSS(newInsets); @@ -239,6 +260,17 @@ private void initWindowInsetsListener() { }); } + private WindowInsetsCompat.Builder withKeyboardInsetsHandling(WindowInsetsCompat.Builder builder) { + if (resizeForKeyboard) { + return builder; + } + + // WebView >= 139 resizes its visual viewport for the IME insets it receives, which would make the page pannable. + // Setting them to `0` rather than consuming them keeps the WebView's inset state up to date. + // See: https://developer.android.com/develop/ui/views/layout/webapps/understand-window-insets + return builder.setInsets(WindowInsetsCompat.Type.ime(), Insets.NONE).setVisible(WindowInsetsCompat.Type.ime(), false); + } + private void injectSafeAreaCSS(WindowInsetsCompat insets) { if (!INSETS_HANDLING_CSS.equals(insetsHandling)) { return; diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 2cee84996..6c508e888 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -784,6 +784,21 @@ export interface PluginsConfig { */ insetsHandling?: 'native' | 'css' | 'disable'; + /** + * Specifies how to handle the keyboard insets on Android. + * + * This option is only supported on Android and has no effect if `insetsHandling` is set to `disable`. + * + * `resize` = Resizes the webview when the keyboard is shown, so the keyboard never covers it. + * + * `none` = Leaves the webview untouched when the keyboard is shown, so the keyboard overlays it. + * This shifts the responsibility of keeping content clear of the keyboard to your own code, + * for example by using the keyboard height reported by `@capacitor/keyboard`. + * + * @default "resize" + */ + keyboardInsetsHandling?: 'resize' | 'none'; + /** * 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`. diff --git a/core/system-bars.md b/core/system-bars.md index 921cc55f1..17e563dd9 100644 --- a/core/system-bars.md +++ b/core/system-bars.md @@ -66,6 +66,7 @@ const setStatusBarAnimation = async () => { | Prop | Type | Description | Default | | ------------- | -------------------- | ------------------------------------------------------------------------- | ------------------ | | **`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 | +| **`keyboardInsetsHandling`** | string | Specifies how to handle the keyboard insets on Android.
This option is only supported on Android and has no effect if `insetsHandling` is set to `disable`.

`resize` = Resizes the webview when the keyboard is shown, so the keyboard never covers it.

`none` = Leaves the webview untouched when the keyboard is shown, so the keyboard overlays it.
This shifts the responsibility of keeping content clear of the keyboard to your own code,
for example by using the keyboard height reported by `@capacitor/keyboard`. | resize | | **`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. | undefined | | **`style`** | string | The style of the text and icons of the system bars. | DEFAULT | | **`hidden`** | boolean | Hide the system bars on start. | false |