From 4e7d766dd4128805067ed538a48f14b81b3a9ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kapa=C5=82a?= Date: Wed, 13 Aug 2025 09:14:46 +0200 Subject: [PATCH 1/2] Update KeyboardAnimationCallback.kt --- .../listeners/KeyboardAnimationCallback.kt | 161 ++++++++++++------ 1 file changed, 111 insertions(+), 50 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 81cd40c4cb..982fce9c22 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -20,12 +20,23 @@ import com.reactnativekeyboardcontroller.extensions.dispatchEvent import com.reactnativekeyboardcontroller.extensions.dp import com.reactnativekeyboardcontroller.extensions.emitEvent import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation -import com.reactnativekeyboardcontroller.extensions.keepShadowNodesInSync import com.reactnativekeyboardcontroller.extensions.keyboardType import com.reactnativekeyboardcontroller.interactive.InteractiveKeyboardProvider import com.reactnativekeyboardcontroller.log.Logger import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder import kotlin.math.abs +import androidx.core.view.OneShotPreDrawListener + +private var preDrawScheduled = false + +private fun deferToPreDrawOnce(target: View, block: () -> Unit) { + if (preDrawScheduled) return + preDrawScheduled = true + OneShotPreDrawListener.add(target) { + preDrawScheduled = false + block() // run with freshest insets right before draw + } +} private val TAG = KeyboardAnimationCallback::class.qualifiedName private val isResizeHandledInCallbackMethods = Keyboard.IS_ANIMATION_EMULATED @@ -50,6 +61,7 @@ class KeyboardAnimationCallback( val view: View, val context: ThemedReactContext?, private val config: KeyboardAnimationCallbackConfig, + val deferFire: Boolean = true ) : WindowInsetsAnimationCompat.Callback(config.dispatchMode), OnApplyWindowInsetsListener, Suspendable { @@ -60,6 +72,7 @@ class KeyboardAnimationCallback( private var prevKeyboardHeight = 0.0 private var isKeyboardVisible = false private var isTransitioning = false + private var isPreparing = false private var duration = 0 private var viewTagFocused = -1 private var animationsToSkip = hashSetOf() @@ -80,32 +93,47 @@ class KeyboardAnimationCallback( // 2. event should be send only when keyboard is visible, since this event arrives earlier -> `tag` will be // 100% included in onStart/onMove/onEnd life cycles, but triggering onStart/onEnd several time // can bring breaking changes - context.dispatchEvent( - eventPropagationView.id, - KeyboardTransitionEvent( - surfaceId, + val lambda = { + context.dispatchEvent( eventPropagationView.id, - KeyboardTransitionEvent.Start, - this.persistentKeyboardHeight, - 1.0, - 0, - viewTagFocused, - ), - ) - context.dispatchEvent( - eventPropagationView.id, - KeyboardTransitionEvent( - surfaceId, + KeyboardTransitionEvent( + surfaceId, + eventPropagationView.id, + KeyboardTransitionEvent.Start, + this.persistentKeyboardHeight, + 1.0, + 0, + viewTagFocused, + ), + ) + context.dispatchEvent( eventPropagationView.id, - KeyboardTransitionEvent.End, - this.persistentKeyboardHeight, - 1.0, - 0, - viewTagFocused, - ), - ) - context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight)) - context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight)) + KeyboardTransitionEvent( + surfaceId, + eventPropagationView.id, + KeyboardTransitionEvent.End, + this.persistentKeyboardHeight, + 1.0, + 0, + viewTagFocused, + ), + ) + context.emitEvent( + "KeyboardController::keyboardWillShow", + getEventParams(this.persistentKeyboardHeight) + ) + context.emitEvent( + "KeyboardController::keyboardDidShow", + getEventParams(this.persistentKeyboardHeight) + ) + } + if (deferFire) { + deferToPreDrawOnce(view) { + lambda() + } + } else { + lambda() + } } } } @@ -135,6 +163,7 @@ class KeyboardAnimationCallback( v: View, insets: WindowInsetsCompat, ): WindowInsetsCompat { + val keyboardHeight = getCurrentKeyboardHeight() // when keyboard appears values will be (false && true) // when keyboard disappears values will be (true && false) @@ -157,15 +186,41 @@ class KeyboardAnimationCallback( // in this method val isKeyboardSizeEqual = this.persistentKeyboardHeight == keyboardHeight + // Handle cases where the Android OS shows/hides the IME without an + // animation, for example when presenting the OS share sheet or in split app mode. + if (!isMoving && !isPreparing) { + Logger.i(TAG, "IME changed without animation – sending synthetic events") + if (deferFire) { + deferToPreDrawOnce(view) { + syncKeyboardPosition(keyboardHeight, isKeyboardVisible()) + } + } else { + syncKeyboardPosition(keyboardHeight, isKeyboardVisible()) + } + } + if (isKeyboardFullyVisible && !isKeyboardSizeEqual && !isResizeHandledInCallbackMethods) { Logger.i(TAG, "onApplyWindowInsets: ${this.persistentKeyboardHeight} -> $keyboardHeight") - layoutObserver?.syncUpLayout() - this.onKeyboardResized(keyboardHeight) + + if (deferFire) { + deferToPreDrawOnce(view) { + layoutObserver?.syncUpLayout() + this.onKeyboardResized(keyboardHeight) + } + } else { + layoutObserver?.syncUpLayout() + this.onKeyboardResized(keyboardHeight) + } } return insets } + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + super.onPrepare(animation) + isPreparing = true + } + @Suppress("detekt:ReturnCount") override fun onStart( animation: WindowInsetsAnimationCompat, @@ -175,6 +230,7 @@ class KeyboardAnimationCallback( return bounds } + isPreparing = false isTransitioning = true isKeyboardVisible = isKeyboardVisible() duration = animation.durationMillis.toInt() @@ -292,7 +348,7 @@ class KeyboardAnimationCallback( if (!animation.isKeyboardAnimation || isSuspended) { return } - + isPreparing = false isTransitioning = false duration = animation.durationMillis.toInt() @@ -328,8 +384,6 @@ class KeyboardAnimationCallback( // reset to initial state duration = 0 - - context.keepShadowNodesInSync(eventPropagationView.id) } if (isKeyboardInteractive) { @@ -384,28 +438,35 @@ class KeyboardAnimationCallback( private fun onKeyboardResized(keyboardHeight: Double) { duration = 0 - context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight)) - listOf( - KeyboardTransitionEvent.Start, - KeyboardTransitionEvent.Move, - KeyboardTransitionEvent.End, - ).forEach { eventName -> - context.dispatchEvent( - eventPropagationView.id, - KeyboardTransitionEvent( - surfaceId, + val lambda = { + context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight)) + listOf( + KeyboardTransitionEvent.Start, + KeyboardTransitionEvent.Move, + KeyboardTransitionEvent.End, + ).forEach { eventName -> + context.dispatchEvent( eventPropagationView.id, - eventName, - keyboardHeight, - 1.0, - 0, - viewTagFocused, - ), - ) + KeyboardTransitionEvent( + surfaceId, + eventPropagationView.id, + eventName, + keyboardHeight, + 1.0, + 0, + viewTagFocused, + ), + ) + } + context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight)) + } + if (deferFire) { + deferToPreDrawOnce(view) { + lambda() + } + } else { + lambda() } - context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight)) - context.keepShadowNodesInSync(eventPropagationView.id) - this.persistentKeyboardHeight = keyboardHeight } From d95d57944c1f88afcac13462e11296bb2085cb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Kapa=C5=82a?= Date: Mon, 18 Aug 2025 12:08:46 +0200 Subject: [PATCH 2/2] Update KeyboardAnimationCallback.kt --- .../listeners/KeyboardAnimationCallback.kt | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 982fce9c22..c161d92435 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -26,6 +26,7 @@ import com.reactnativekeyboardcontroller.log.Logger import com.reactnativekeyboardcontroller.traversal.FocusedInputHolder import kotlin.math.abs import androidx.core.view.OneShotPreDrawListener +import android.util.Log private var preDrawScheduled = false @@ -230,6 +231,8 @@ class KeyboardAnimationCallback( return bounds } + Log.v("Keyboard xxxxx", "onStart start"); + isPreparing = false isTransitioning = true isKeyboardVisible = isKeyboardVisible() @@ -241,7 +244,13 @@ class KeyboardAnimationCallback( this.persistentKeyboardHeight = keyboardHeight } - layoutObserver?.syncUpLayout() + if (deferFire) { + deferToPreDrawOnce(view) { + layoutObserver?.syncUpLayout() + } + } else { + layoutObserver?.syncUpLayout() + } // keyboard gets resized - we do not want to have a default animated transition // so we skip these animations @@ -251,28 +260,42 @@ class KeyboardAnimationCallback( onKeyboardResized(keyboardHeight) animationsToSkip.add(animation) + Log.v("Keyboard xxxxx", "onStart end"); return bounds } - context.emitEvent( - "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow", - getEventParams(keyboardHeight), - ) + val lambda = { + context.emitEvent( + "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow", + getEventParams(keyboardHeight), + ) - Logger.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused") - context.dispatchEvent( - eventPropagationView.id, - KeyboardTransitionEvent( - surfaceId, + Logger.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused") + context.dispatchEvent( eventPropagationView.id, - KeyboardTransitionEvent.Start, - keyboardHeight, - if (!isKeyboardVisible) 0.0 else 1.0, - duration, - viewTagFocused, - ), - ) + KeyboardTransitionEvent( + surfaceId, + eventPropagationView.id, + KeyboardTransitionEvent.Start, + keyboardHeight, + if (!isKeyboardVisible) 0.0 else 1.0, + duration, + viewTagFocused, + ), + ) + } + + if (deferFire) { + deferToPreDrawOnce(view) { + lambda() + } + } else { + lambda() + } + + + Log.v("Keyboard xxxxx", "onStart end"); return super.onStart(animation, bounds) }