From 9d9d22c6bb018ac19ccd1faa28856c3648f7f56e Mon Sep 17 00:00:00 2001 From: ish1416 Date: Sat, 8 Nov 2025 03:30:06 +0530 Subject: [PATCH 1/2] fix: prevent keyboard scroll space on Android 16 devices - Exclude IME insets from bottom margin calculation in CapacitorWebView - Add keyboard handling configuration to Android template - Fixes extra scroll space equal to keyboard height when keyboard closes --- android-template/app/src/main/AndroidManifest.xml | 1 + .../java/com/getcapacitor/CapacitorWebView.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/android-template/app/src/main/AndroidManifest.xml b/android-template/app/src/main/AndroidManifest.xml index b06ddbfd8..79881d678 100644 --- a/android-template/app/src/main/AndroidManifest.xml +++ b/android-template/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:label="@string/title_activity_main" android:theme="@style/AppTheme.NoActionBarLaunch" android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize" android:exported="true"> diff --git a/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java b/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java index 07c9ec971..f9717a015 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java +++ b/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java @@ -12,6 +12,7 @@ import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; +import android.view.ViewGroup.MarginLayoutParams; public class CapacitorWebView extends WebView { @@ -73,7 +74,19 @@ public void edgeToEdgeHandler(Bridge bridge) { if (forceMargins || autoMargins) { ViewCompat.setOnApplyWindowInsetsListener(this, (v, windowInsets) -> { + // Exclude IME (keyboard) insets to prevent extra scroll space Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + + // Check if keyboard configuration should prevent resizing + boolean preventKeyboardResize = bridge.getConfig().isInputCaptured(); + if (preventKeyboardResize) { + // Don't apply bottom margin when keyboard is visible to prevent scroll space + Insets imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()); + if (imeInsets.bottom > 0) { + insets = Insets.of(insets.left, insets.top, insets.right, 0); + } + } + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; mlp.bottomMargin = insets.bottom; From 56c175975daf8fc31bb7280e275d5017c6bbc288 Mon Sep 17 00:00:00 2001 From: ish1416 Date: Sat, 8 Nov 2025 11:37:57 +0530 Subject: [PATCH 2/2] fix: resolve Android 15+ keyboard overlap with bottom inputs - Enhanced edge-to-edge handler for Android 15+ (API 34+) - Added keyboard visibility detection and auto-scroll to focused inputs - Proper IME insets handling to prevent input field overlap - Maintains compatibility with older Android versions - Fixes keyboard covering bottom inputs in edge-to-edge mode --- .../com/getcapacitor/CapacitorWebView.java | 101 ++++++++++++++---- 1 file changed, 83 insertions(+), 18 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java b/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java index f9717a015..de9b3d13a 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java +++ b/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java @@ -13,6 +13,7 @@ import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import android.view.ViewGroup.MarginLayoutParams; +import android.os.Build; public class CapacitorWebView extends WebView { @@ -26,6 +27,33 @@ public CapacitorWebView(Context context, AttributeSet attrs) { public void setBridge(Bridge bridge) { this.bridge = bridge; } + + /** + * Handle keyboard visibility changes to ensure input fields remain visible + */ + private void handleKeyboardVisibility(boolean isVisible, int keyboardHeight) { + if (Build.VERSION.SDK_INT >= 34) { + post(() -> { + if (isVisible) { + // Scroll to focused element when keyboard appears + evaluateJavascript( + "(function() {" + + " var focused = document.activeElement;" + + " if (focused && (focused.tagName === 'INPUT' || focused.tagName === 'TEXTAREA')) {" + + " var rect = focused.getBoundingClientRect();" + + " var viewportHeight = window.innerHeight;" + + " var keyboardHeight = " + keyboardHeight + ";" + + " if (rect.bottom > (viewportHeight - keyboardHeight)) {" + + " var scrollOffset = rect.bottom - (viewportHeight - keyboardHeight) + 20;" + + " window.scrollBy(0, scrollOffset);" + + " }" + + " }" + + "})();", null + ); + } + }); + } + } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { @@ -64,37 +92,74 @@ public void edgeToEdgeHandler(Bridge bridge) { boolean autoMargins = false; boolean forceMargins = configEdgeToEdge.equals("force"); + // Handle Android 15+ edge-to-edge enforcement if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && configEdgeToEdge.equals("auto")) { TypedValue value = new TypedValue(); boolean foundOptOut = getContext().getTheme().resolveAttribute(android.R.attr.windowOptOutEdgeToEdgeEnforcement, value, true); - boolean optOutValue = value.data != 0; // value is set to -1 on true as of Android 15, so we have to do this. - + boolean optOutValue = value.data != 0; autoMargins = !(foundOptOut && optOutValue); } + + // Force margins for Android 15+ when edge-to-edge is enabled + if (Build.VERSION.SDK_INT >= 34 && configEdgeToEdge.equals("force")) { + forceMargins = true; + } if (forceMargins || autoMargins) { ViewCompat.setOnApplyWindowInsetsListener(this, (v, windowInsets) -> { - // Exclude IME (keyboard) insets to prevent extra scroll space - Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + Insets systemInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + Insets imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()); - // Check if keyboard configuration should prevent resizing - boolean preventKeyboardResize = bridge.getConfig().isInputCaptured(); - if (preventKeyboardResize) { - // Don't apply bottom margin when keyboard is visible to prevent scroll space - Insets imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()); + // Android 15+ keyboard handling + if (Build.VERSION.SDK_INT >= 34) { + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + + // Apply system insets + mlp.leftMargin = systemInsets.left; + mlp.rightMargin = systemInsets.right; + mlp.topMargin = systemInsets.top; + + // Handle keyboard visibility for Android 15+ if (imeInsets.bottom > 0) { - insets = Insets.of(insets.left, insets.top, insets.right, 0); + // Keyboard is visible - ensure input visibility + handleKeyboardVisibility(true, imeInsets.bottom); + if (bridge.getConfig().isInputCaptured()) { + // Don't apply bottom margin to prevent scroll space + mlp.bottomMargin = 0; + // Adjust WebView height to account for keyboard + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), imeInsets.bottom); + } else { + mlp.bottomMargin = Math.max(systemInsets.bottom, imeInsets.bottom); + } + } else { + // Keyboard hidden - restore normal margins + handleKeyboardVisibility(false, 0); + mlp.bottomMargin = systemInsets.bottom; + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), 0); + } + + v.setLayoutParams(mlp); + } else { + // Legacy handling for older Android versions + MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); + mlp.leftMargin = systemInsets.left; + mlp.rightMargin = systemInsets.right; + mlp.topMargin = systemInsets.top; + + if (bridge.getConfig().isInputCaptured() && imeInsets.bottom > 0) { + mlp.bottomMargin = 0; + } else { + mlp.bottomMargin = systemInsets.bottom; } + + v.setLayoutParams(mlp); } - - MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams(); - mlp.leftMargin = insets.left; - mlp.bottomMargin = insets.bottom; - mlp.rightMargin = insets.right; - mlp.topMargin = insets.top; - v.setLayoutParams(mlp); - // Don't pass window insets to children + // Return appropriate insets based on Android version + if (Build.VERSION.SDK_INT >= 34 && imeInsets.bottom > 0) { + // On Android 15+, let the system handle IME insets + return windowInsets.inset(systemInsets); + } return WindowInsetsCompat.CONSUMED; }); }