Fix crash when using View.hideKeyboardAndAwaitAnimation (#6502)

* Fix crash when using `View.hideKeyboardAndAwaitAnimation`

Remove the `View.OnApplyWindowInsetsListener` used in modern Android versions to detect if the insets changed after they do the first time: this is a single use operation and the listener will be called every time the insets change

Also, replace `Mutex` with `CompletableDeferred` so it doesn't matter if it's called several times, we only care about the first one.

* Don't try to hide the keyboard if it's already hidden. Also, add a 1s timeout in case everything goes wrong and we somehow never complete the future.
This commit is contained in:
Jorge Martin Espinosa 2026-03-31 18:45:10 +02:00 committed by GitHub
parent dbd51ebc90
commit 786c260fc2

View file

@ -16,9 +16,11 @@ import android.view.ViewTreeObserver
import android.view.WindowInsets import android.view.WindowInsets
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeoutOrNull
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
fun View.hideKeyboard() { fun View.hideKeyboard() {
val imm = context?.getSystemService<InputMethodManager>() val imm = context?.getSystemService<InputMethodManager>()
@ -26,29 +28,39 @@ fun View.hideKeyboard() {
} }
suspend fun View.hideKeyboardAndAwaitAnimation() { suspend fun View.hideKeyboardAndAwaitAnimation() {
val imm = context?.getSystemService<InputMethodManager>() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !rootWindowInsets.isVisible(WindowInsets.Type.ime())) {
// Keyboard is already hidden, no need to do anything
return
}
val mutex = Mutex() val imm = context?.getSystemService<InputMethodManager>() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val future = CompletableDeferred<Unit>()
val requested = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setOnApplyWindowInsetsListener { view, insets -> setOnApplyWindowInsetsListener { view, insets ->
if (!insets.isVisible(WindowInsets.Type.ime())) { if (!insets.isVisible(WindowInsets.Type.ime())) {
mutex.unlock() future.complete(Unit)
// Remove the listener now, it's a single use operation
setOnApplyWindowInsetsListener(null)
} }
insets insets
} }
imm?.hideSoftInputFromWindow(windowToken, 0) imm.hideSoftInputFromWindow(windowToken, 0)
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
imm?.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) { imm.hideSoftInputFromWindow(windowToken, 0, object : ResultReceiver(null) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) { override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN || if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN || resultCode == InputMethodManager.RESULT_HIDDEN) {
resultCode == InputMethodManager.RESULT_HIDDEN) { future.complete(Unit)
mutex.unlock()
} }
} }
}) })
} }
mutex.lock()
if (requested) {
// Await the future to ensure the keyboard hide animation has completed before proceeding
withTimeoutOrNull(1.seconds) { future.await() }
}
} }
fun View.showKeyboard(andRequestFocus: Boolean = false) { fun View.showKeyboard(andRequestFocus: Boolean = false) {