Skip to content

Avoid Illegal states #1083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,24 @@ 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
import android.util.Log

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
Expand All @@ -50,6 +62,7 @@ class KeyboardAnimationCallback(
val view: View,
val context: ThemedReactContext?,
private val config: KeyboardAnimationCallbackConfig,
val deferFire: Boolean = true
) : WindowInsetsAnimationCompat.Callback(config.dispatchMode),
OnApplyWindowInsetsListener,
Suspendable {
Expand All @@ -60,6 +73,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<WindowInsetsAnimationCompat>()
Expand All @@ -80,32 +94,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()
}
}
}
}
Expand Down Expand Up @@ -135,6 +164,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)
Expand All @@ -157,15 +187,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,
Expand All @@ -175,6 +231,9 @@ class KeyboardAnimationCallback(
return bounds
}

Log.v("Keyboard xxxxx", "onStart start");

isPreparing = false
isTransitioning = true
isKeyboardVisible = isKeyboardVisible()
duration = animation.durationMillis.toInt()
Expand All @@ -185,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
Expand All @@ -195,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)
}

Expand Down Expand Up @@ -292,7 +371,7 @@ class KeyboardAnimationCallback(
if (!animation.isKeyboardAnimation || isSuspended) {
return
}

isPreparing = false
isTransitioning = false
duration = animation.durationMillis.toInt()

Expand Down Expand Up @@ -328,8 +407,6 @@ class KeyboardAnimationCallback(

// reset to initial state
duration = 0

context.keepShadowNodesInSync(eventPropagationView.id)
}

if (isKeyboardInteractive) {
Expand Down Expand Up @@ -384,28 +461,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
}

Expand Down