From a45270fd1c97dd2fa3135103a604c90c19315f1a Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 14 Jul 2025 00:02:12 +0200 Subject: [PATCH 1/3] fix: double synchronization --- .../listeners/KeyboardAnimationCallback.kt | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 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..7e8dd27c5b 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -161,11 +161,34 @@ class KeyboardAnimationCallback( Logger.i(TAG, "onApplyWindowInsets: ${this.persistentKeyboardHeight} -> $keyboardHeight") layoutObserver?.syncUpLayout() this.onKeyboardResized(keyboardHeight) + + return insets + } + + // always verify insets, because sometimes default lifecycles may not be invoked + // (when we press "Share" on Android 16, for example) + val newHeight = getCurrentKeyboardHeight(insets) + if (prevKeyboardHeight != newHeight && !isTransitioning) { + Logger.w( + TAG, + "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight" + ) + this.syncKeyboardPosition(newHeight, newHeight > 0) } return insets } + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + super.onPrepare(animation) + + if (!animation.isKeyboardAnimation || isSuspended) { + return + } + + isTransitioning = true + } + @Suppress("detekt:ReturnCount") override fun onStart( animation: WindowInsetsAnimationCompat, @@ -175,7 +198,6 @@ class KeyboardAnimationCallback( return bounds } - isTransitioning = true isKeyboardVisible = isKeyboardVisible() duration = animation.durationMillis.toInt() val keyboardHeight = getCurrentKeyboardHeight() @@ -415,14 +437,15 @@ class KeyboardAnimationCallback( return insets?.isVisible(WindowInsetsCompat.Type.ime()) ?: false } - private fun getCurrentKeyboardHeight(): Double { - val insets = ViewCompat.getRootWindowInsets(view) - val keyboardHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 + private fun getCurrentKeyboardHeight(insets: WindowInsetsCompat? = null): Double { + val root = ViewCompat.getRootWindowInsets(view) + val final = insets ?: root + val keyboardHeight = final?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 val navigationBar = if (config.hasTranslucentNavigationBar) { 0 } else { - insets?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0 + root?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0 } // on hide it will be negative value, so we are using max function From 30295634f929250b4b0068d161502d1518872f7e Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 16 Jul 2025 20:01:17 +0200 Subject: [PATCH 2/3] fix: modal conflicts --- .../listeners/KeyboardAnimationCallback.kt | 16 ++++++++++---- .../modal/ModalAttachedWatcher.kt | 22 +++++++++---------- cspell.json | 3 ++- 3 files changed, 24 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 7e8dd27c5b..47f53f1ffb 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -50,6 +50,7 @@ class KeyboardAnimationCallback( val view: View, val context: ThemedReactContext?, private val config: KeyboardAnimationCallbackConfig, + private val source: KeyboardAnimationCallback? = null, ) : WindowInsetsAnimationCompat.Callback(config.dispatchMode), OnApplyWindowInsetsListener, Suspendable { @@ -67,6 +68,13 @@ class KeyboardAnimationCallback( get() = duration == -1 override var isSuspended: Boolean = false + init { + if (source != null) { + this.persistentKeyboardHeight = source.persistentKeyboardHeight + this.prevKeyboardHeight = source.prevKeyboardHeight + } + } + // listeners private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus -> @@ -165,13 +173,13 @@ class KeyboardAnimationCallback( return insets } - // always verify insets, because sometimes default lifecycles may not be invoked - // (when we press "Share" on Android 16, for example) + // always verify insets, because sometimes default lifecycle methods may not be invoked + // (when we press "Share" on Android 16, when Modal closes keyboard, etc.) val newHeight = getCurrentKeyboardHeight(insets) - if (prevKeyboardHeight != newHeight && !isTransitioning) { + if (prevKeyboardHeight != newHeight && !isMoving && !isSuspended) { Logger.w( TAG, - "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight" + "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight. Modal ${this.source}", ) this.syncKeyboardPosition(newHeight, newHeight > 0) } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt index 32cbf45260..180ae39301 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt @@ -35,6 +35,7 @@ class ModalAttachedWatcher( return } + val cb = this.callback() val modal = try { uiManager?.resolveView(event.viewTag) as? ReactModalHostView @@ -46,6 +47,9 @@ class ModalAttachedWatcher( if (modal == null) { return } + if (cb == null) { + return + } val dialog = modal.dialog val window = dialog?.window @@ -62,6 +66,7 @@ class ModalAttachedWatcher( eventPropagationView = view, context = reactContext, config = config, + source = cb, ) rootView.addView(eventView) @@ -70,28 +75,21 @@ class ModalAttachedWatcher( // on Android < 12 all events for `WindowInsetsAnimationCallback` // go through main `rootView`, so we don't need to stop main // callback - otherwise keyboard transitions will not be animated - this.callback()?.suspend(true) + cb.suspend(true) // attaching callback to Modal on Android < 12 can cause ghost animations, see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/718 // and overall attaching additional callbacks (if animation events go through the main window) is not necessary ViewCompat.setWindowInsetsAnimationCallback(rootView, callback) ViewCompat.setOnApplyWindowInsetsListener(eventView, callback) - - // when modal is shown then keyboard will be hidden by default - // - // - if events are coming from main window - then keyboard position - // will be synchronized from main window callback - // - if events are coming from modal window - then we need to update - // position ourself, because callback can be attached after keyboard - // auto-dismissal and we may miss some events and keyboard position - // will be outdated - callback.syncKeyboardPosition(0.0, false) } dialog?.setOnDismissListener { callback.syncKeyboardPosition() callback.destroy() eventView.removeSelf() - this.callback()?.suspend(false) + // un-pause it in next frame because straight away `onApplyWindowInsets` will be called + view.post { + this.callback()?.suspend(false) + } } // imitating edge-to-edge mode behavior diff --git a/cspell.json b/cspell.json index c22b1a20cd..b86415b042 100644 --- a/cspell.json +++ b/cspell.json @@ -170,7 +170,8 @@ "Pixelfed", "Kwai", "Kwibo", - "revolut" + "revolut", + "desynchronized" ], "ignorePaths": [ "node_modules", From 0cfd04976780e7eeea88ff7dd8c835fd2cdc3e73 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 28 Jul 2025 12:20:33 +0200 Subject: [PATCH 3/3] fix: make code organization slightly better --- .../listeners/KeyboardAnimationCallback.kt | 15 +++++---------- .../modal/ModalAttachedWatcher.kt | 3 ++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 47f53f1ffb..b31e27b557 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -57,8 +57,8 @@ class KeyboardAnimationCallback( private val surfaceId = UIManagerHelper.getSurfaceId(eventPropagationView) // state variables - private var persistentKeyboardHeight = 0.0 - private var prevKeyboardHeight = 0.0 + private var persistentKeyboardHeight = getCurrentKeyboardHeight() + private var prevKeyboardHeight = getCurrentKeyboardHeight() private var isKeyboardVisible = false private var isTransitioning = false private var duration = 0 @@ -68,13 +68,6 @@ class KeyboardAnimationCallback( get() = duration == -1 override var isSuspended: Boolean = false - init { - if (source != null) { - this.persistentKeyboardHeight = source.persistentKeyboardHeight - this.prevKeyboardHeight = source.prevKeyboardHeight - } - } - // listeners private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus -> @@ -179,7 +172,7 @@ class KeyboardAnimationCallback( if (prevKeyboardHeight != newHeight && !isMoving && !isSuspended) { Logger.w( TAG, - "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight. Modal ${this.source}", + "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight. Attached: ${view.isAttachedToWindow} EVA: ${this.eventPropagationView.isAttachedToWindow} Modal ${this.source}", ) this.syncKeyboardPosition(newHeight, newHeight > 0) } @@ -190,6 +183,8 @@ class KeyboardAnimationCallback( override fun onPrepare(animation: WindowInsetsAnimationCompat) { super.onPrepare(animation) + println("desynchronized - onPrepare") + if (!animation.isKeyboardAnimation || isSuspended) { return } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt index 180ae39301..ee8f528dd3 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt @@ -77,7 +77,8 @@ class ModalAttachedWatcher( // callback - otherwise keyboard transitions will not be animated cb.suspend(true) // attaching callback to Modal on Android < 12 can cause ghost animations, see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/718 - // and overall attaching additional callbacks (if animation events go through the main window) is not necessary + // and overall attaching additional callbacks (if animation events go through the main window) + // is not necessary ViewCompat.setWindowInsetsAnimationCallback(rootView, callback) ViewCompat.setOnApplyWindowInsetsListener(eventView, callback) }