diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 81cd40c4cb..b31e27b557 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -50,14 +50,15 @@ class KeyboardAnimationCallback( val view: View, val context: ThemedReactContext?, private val config: KeyboardAnimationCallbackConfig, + private val source: KeyboardAnimationCallback? = null, ) : WindowInsetsAnimationCompat.Callback(config.dispatchMode), OnApplyWindowInsetsListener, Suspendable { 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 @@ -161,11 +162,36 @@ class KeyboardAnimationCallback( Logger.i(TAG, "onApplyWindowInsets: ${this.persistentKeyboardHeight} -> $keyboardHeight") layoutObserver?.syncUpLayout() this.onKeyboardResized(keyboardHeight) + + return insets + } + + // 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 && !isMoving && !isSuspended) { + Logger.w( + TAG, + "detected desynchronized state - force updating it. $prevKeyboardHeight -> $newHeight. Attached: ${view.isAttachedToWindow} EVA: ${this.eventPropagationView.isAttachedToWindow} Modal ${this.source}", + ) + this.syncKeyboardPosition(newHeight, newHeight > 0) } return insets } + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + super.onPrepare(animation) + + println("desynchronized - onPrepare") + + if (!animation.isKeyboardAnimation || isSuspended) { + return + } + + isTransitioning = true + } + @Suppress("detekt:ReturnCount") override fun onStart( animation: WindowInsetsAnimationCompat, @@ -175,7 +201,6 @@ class KeyboardAnimationCallback( return bounds } - isTransitioning = true isKeyboardVisible = isKeyboardVisible() duration = animation.durationMillis.toInt() val keyboardHeight = getCurrentKeyboardHeight() @@ -415,14 +440,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 diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt b/android/src/main/java/com/reactnativekeyboardcontroller/modal/ModalAttachedWatcher.kt index 32cbf45260..ee8f528dd3 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,22 @@ 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 + // 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",