diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java
index 7cf87fc7ef661c2a13fb7a5f2c5c89df99bd730c..53d8abc7d634c587379454afe5f31644883ec28a 100644
--- a/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java
+++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/Keyboard.java
@@ -31,6 +31,26 @@ public class Keyboard {
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private View mChildOfContent;
+ private int lastImeHeight = 0;
+ public enum EventMode {
+ DEFAULT,
+ LAST_KNOWN
+ }
+
+ private boolean isAnimating = false;
+ private boolean justEndedAnimation = false;
+ private int knownKeyboardHeight = 0;
+ private EventMode eventMode = EventMode.DEFAULT;
+
+ public void setEventMode(String modeStr) {
+ if (modeStr != null) {
+ try {
+ this.eventMode = EventMode.valueOf(modeStr.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ this.eventMode = EventMode.DEFAULT;
+ }
+ }
+ }
public void setKeyboardEventListener(@Nullable KeyboardEventListener keyboardEventListener) {
this.keyboardEventListener = keyboardEventListener;
@@ -59,26 +79,85 @@ public class Keyboard {
rootView = content.getRootView();
ViewCompat.setOnApplyWindowInsetsListener(content, (v, insets) -> {
- boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime());
+ boolean showingKeyboard = insets.isVisible(WindowInsetsCompat.Type.ime());
+ int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
+ DisplayMetrics dm = activity.getResources().getDisplayMetrics();
+ final float density = dm.density;
+
+ if (showingKeyboard) {
+ int currentImeHeight = Math.round(imeHeight / density);
+ if (!isAnimating) {
+ int emitHeight = currentImeHeight;
+ if (eventMode == EventMode.LAST_KNOWN) {
+ if (knownKeyboardHeight == 0) {
+ knownKeyboardHeight = currentImeHeight;
+ } else if (justEndedAnimation) {
+ knownKeyboardHeight = currentImeHeight;
+ } else if (currentImeHeight > knownKeyboardHeight) {
+ emitHeight = knownKeyboardHeight;
+ } else if (currentImeHeight < knownKeyboardHeight) {
+ knownKeyboardHeight = currentImeHeight;
+ }
+ } else {
+ knownKeyboardHeight = currentImeHeight;
+ }
+
+ if (emitHeight != lastImeHeight && keyboardEventListener != null) {
+ lastImeHeight = emitHeight;
+ // Fire WILL_SHOW immediately from onApplyWindowInsets for a 28ms CSS head-start
+ keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_SHOW, emitHeight);
+ keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_SHOW, emitHeight);
+ }
+ justEndedAnimation = false;
+ } else if (eventMode == EventMode.LAST_KNOWN) {
+ // Even if animating, if we don't have a known height yet, grab it.
+ if (knownKeyboardHeight == 0) {
+ knownKeyboardHeight = currentImeHeight;
+ }
+ }
+ } else {
+ lastImeHeight = 0;
+ justEndedAnimation = false;
+ }
+
+ logDebug("onApplyWindowInsets - showingKeyboard=" + showingKeyboard, insets, imeHeight);
if (showingKeyboard && resizeOnFullScreen) {
possiblyResizeChildOfContent(true);
+ } else if (!showingKeyboard && resizeOnFullScreen) {
+ possiblyResizeChildOfContent(false);
}
- v.onApplyWindowInsets(insets.toWindowInsets());
+ WindowInsetsCompat insetsToApply = insets;
+ if (!resizeOnFullScreen) {
+ insetsToApply = new WindowInsetsCompat.Builder(insets)
+ .setInsets(WindowInsetsCompat.Type.ime(), androidx.core.graphics.Insets.NONE)
+ .build();
+ }
- return insets;
+ return ViewCompat.onApplyWindowInsets(v, insetsToApply);
});
ViewCompat.setWindowInsetsAnimationCallback(
rootView,
new WindowInsetsAnimationCompat.Callback(WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
+ @Override
+ public void onPrepare(@NonNull WindowInsetsAnimationCompat animation) {
+ isAnimating = true;
+ super.onPrepare(animation);
+ }
+
@NonNull
@Override
public WindowInsetsCompat onProgress(
@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations
) {
+ if (!resizeOnFullScreen) {
+ return new WindowInsetsCompat.Builder(insets)
+ .setInsets(WindowInsetsCompat.Type.ime(), androidx.core.graphics.Insets.NONE)
+ .build();
+ }
return insets;
}
@@ -88,6 +167,8 @@ public class Keyboard {
@NonNull WindowInsetsAnimationCompat animation,
@NonNull WindowInsetsAnimationCompat.BoundsCompat bounds
) {
+ isAnimating = true;
+ justEndedAnimation = false;
boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime());
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(rootView);
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
@@ -99,16 +180,31 @@ public class Keyboard {
}
if (showingKeyboard) {
- keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_SHOW, Math.round(imeHeight / density));
+ int currentImeHeight = Math.round(imeHeight / density);
+ int emitHeight = currentImeHeight;
+ if (eventMode == EventMode.LAST_KNOWN && knownKeyboardHeight > 0 && currentImeHeight > knownKeyboardHeight) {
+ emitHeight = knownKeyboardHeight;
+ }
+
+ if (emitHeight != lastImeHeight) {
+ lastImeHeight = emitHeight;
+ keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_SHOW, lastImeHeight);
+ }
} else {
+ lastImeHeight = 0;
keyboardEventListener.onKeyboardEvent(EVENT_KB_WILL_HIDE, 0);
}
+
+ logDebug("onStart - showingKeyboard=" + showingKeyboard, insets, imeHeight);
+
return super.onStart(animation, bounds);
}
@Override
public void onEnd(@NonNull WindowInsetsAnimationCompat animation) {
super.onEnd(animation);
+ isAnimating = false;
+ justEndedAnimation = true;
boolean showingKeyboard = ViewCompat.getRootWindowInsets(rootView).isVisible(WindowInsetsCompat.Type.ime());
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(rootView);
int imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
@@ -116,10 +212,27 @@ public class Keyboard {
final float density = dm.density;
if (showingKeyboard) {
- keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_SHOW, Math.round(imeHeight / density));
+ int currentImeHeight = Math.round(imeHeight / density);
+ int emitHeight = currentImeHeight;
+ if (eventMode == EventMode.LAST_KNOWN && knownKeyboardHeight > 0 && currentImeHeight > knownKeyboardHeight) {
+ emitHeight = knownKeyboardHeight;
+ }
+
+ if (emitHeight != lastImeHeight) {
+ lastImeHeight = emitHeight;
+ keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_SHOW, lastImeHeight);
+ }
+
+ // Update the known keyboard height to the final settled height after animation
+ if (eventMode == EventMode.LAST_KNOWN) {
+ knownKeyboardHeight = currentImeHeight;
+ }
} else {
+ lastImeHeight = 0;
keyboardEventListener.onKeyboardEvent(EVENT_KB_DID_HIDE, 0);
}
+
+ logDebug("onEnd - showingKeyboard=" + showingKeyboard, insets, imeHeight);
}
}
);
@@ -179,4 +292,23 @@ public class Keyboard {
(window.getDecorView().getSystemUiVisibility() & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
);
}
+
+ private void logDebug(String context, WindowInsetsCompat insets, int imeHeight) {
+ if (bridge == null || bridge.getWebView() == null) return;
+ Rect r = new Rect();
+ mChildOfContent.getWindowVisibleDisplayFrame(r);
+ int screenHeight = rootView.getRootView().getHeight();
+ int navBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
+ int statusTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
+ int sysBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
+ int displayCutoutTop = insets.getInsets(WindowInsetsCompat.Type.displayCutout()).top;
+
+ String logStr = String.format(
+ "console.log('[KeyboardDebug] %s: ime.bottom=%d, nav.bottom=%d, status.top=%d, sys.bottom=%d, cutout.top=%d, r.bottom=%d, r.height=%d, screenHeight=%d');",
+ context, imeHeight, navBottom, statusTop, sysBottom, displayCutoutTop, r.bottom, r.height(), screenHeight
+ );
+ bridge.getActivity().runOnUiThread(() -> {
+ bridge.getWebView().evaluateJavascript(logStr, null);
+ });
+ }
}
diff --git a/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java b/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java
index bf6735a85ff72fea5fe4d84b24d682d4080a95d6..4308cb699697f02413255f69a7e5097c04fda0ab 100644
--- a/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java
+++ b/android/src/main/java/com/capacitorjs/plugins/keyboard/KeyboardPlugin.java
@@ -17,7 +17,9 @@ public class KeyboardPlugin extends Plugin {
public void load() {
execute(() -> {
boolean resizeOnFullScreen = getConfig().getBoolean("resizeOnFullScreen", false);
+ String eventMode = getConfig().getString("eventMode", "DEFAULT");
implementation = new Keyboard(getBridge(), resizeOnFullScreen);
+ implementation.setEventMode(eventMode);
implementation.setKeyboardEventListener(this::onKeyboardEvent);
});
diff --git a/dist/esm/definitions.d.ts b/dist/esm/definitions.d.ts
index ceae326dfcbd47c0ebbff2b721c60fb9f52c41f9..23869fdae0535e46b8abe0d33c5fe440d9388c62 100644
--- a/dist/esm/definitions.d.ts
+++ b/dist/esm/definitions.d.ts
@@ -36,9 +36,19 @@ declare module '@capacitor/cli' {
* @example true
*/
resizeOnFullScreen?: boolean;
+ /**
+ * The event mode determines how the plugin emits lifecycle events during keyboard animations.
+ *
+ * Only available for Android
+ *
+ * @since 8.0.3
+ * @example "LAST_KNOWN"
+ */
+ eventMode?: KeyboardEventMode;
};
}
}
+export declare type KeyboardEventMode = "DEFAULT" | "LAST_KNOWN";
export interface KeyboardInfo {
/**
* Height of the keyboard.
Inconsistent Keyboard Height on Android during WindowInsetsAnimation (Proposed Patch)
Bug Description
On Android, the
@capacitor/keyboardplugin can report inconsistent or incorrect keyboard heights (sometimes smaller than the actual keyboard, or0) during keyboard animations or whenWindowInsetsare applied. This is particularly noticeable on newer Android versions (14/15) or when using edge-to-edge displays. The eventskeyboardWillShowandkeyboardDidShowmay fire with intermediate sizes instead of the final settled keyboard height, causing UI jumps or layout issues.This issue is very similar to the previously reported (and closed) Issue #2205 (Keyboard height is inconsistent).
Workaround / Proposed Patch
To solve this, we have developed a patch for
@capacitor/keyboard@8.0.3that introduces a new configuration option:eventMode: "LAST_KNOWN".What the patch does:
knownKeyboardHeightto avoid emitting intermediate sizes during theWindowInsetsAnimationCompatlifecycle (onPrepare,onProgress,onStart,onEnd).WILL_SHOWandDID_SHOWif the height actually changes, falling back to theLAST_KNOWNheight instead of transient smaller heights during animation.onApplyWindowInsets: Grabs a 28ms CSS head-start by firingWILL_SHOWimmediately when insets apply.Configuration (Added in Patch)
Attached Patch
The patch file
@capacitor__keyboard@8.0.3.patch.zipis attached to this issue. It contains the diff forKeyboard.java,KeyboardPlugin.java, anddefinitions.d.ts.Click here to view the patch file