diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt b/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt index 192db3aaa7..0504491ee5 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/events/KeyboardTransitionEvent.kt @@ -4,25 +4,29 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap import com.facebook.react.uimanager.events.Event +data class KeyboardTransitionEventData( + val event: String, + val height: Double, + val progress: Double, + val duration: Int, + val target: Int, +) + @Suppress("detekt:LongParameterList") class KeyboardTransitionEvent( surfaceId: Int, viewId: Int, - private val event: String, - private val height: Double, - private val progress: Double, - private val duration: Int, - private val target: Int, + private val data: KeyboardTransitionEventData, ) : Event(surfaceId, viewId) { - override fun getEventName() = event + override fun getEventName() = data.event // All events for a given view can be coalesced? override fun getCoalescingKey(): Short = 0 override fun getEventData(): WritableMap? = Arguments.createMap().apply { - putDouble("progress", progress) - putDouble("height", height) - putInt("duration", duration) - putInt("target", target) + putDouble("progress", data.progress) + putDouble("height", data.height) + putInt("duration", data.duration) + putInt("target", data.target) } } diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 1bb4f01014..a6aaa67313 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -20,6 +20,7 @@ import com.facebook.react.views.textinput.ReactEditText import com.facebook.react.views.view.ReactViewGroup import com.reactnativekeyboardcontroller.InteractiveKeyboardProvider import com.reactnativekeyboardcontroller.events.KeyboardTransitionEvent +import com.reactnativekeyboardcontroller.events.KeyboardTransitionEventData import com.reactnativekeyboardcontroller.extensions.dispatchEvent import com.reactnativekeyboardcontroller.extensions.dp import kotlin.math.abs @@ -42,6 +43,7 @@ class KeyboardAnimationCallback( private var duration = 0 private var viewTagFocused = -1 private var animation: ValueAnimator? = null + private var lastEventDispatched: KeyboardTransitionEventData? = null // listeners private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus -> @@ -55,11 +57,8 @@ 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 lifecycles, but triggering onStart/onEnd several time // can bring breaking changes - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveStart", this.persistentKeyboardHeight, 1.0, @@ -67,11 +66,8 @@ class KeyboardAnimationCallback( viewTagFocused, ), ) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveEnd", this.persistentKeyboardHeight, 1.0, @@ -159,11 +155,8 @@ class KeyboardAnimationCallback( ) Log.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused") - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveStart", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0, @@ -207,11 +200,8 @@ class KeyboardAnimationCallback( ) val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove" - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( event, height, progress, @@ -247,11 +237,8 @@ class KeyboardAnimationCallback( "KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow", getEventParams(keyboardHeight), ) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveEnd", keyboardHeight, if (!isKeyboardVisible) 0.0 else 1.0, @@ -288,11 +275,8 @@ class KeyboardAnimationCallback( } this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight)) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveStart", keyboardHeight, 1.0, @@ -305,11 +289,8 @@ class KeyboardAnimationCallback( ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat()) animation.addUpdateListener { animator -> val toValue = animator.animatedValue as Float - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMove", toValue.toDouble(), toValue.toDouble() / keyboardHeight, @@ -320,11 +301,8 @@ class KeyboardAnimationCallback( } animation.doOnEnd { this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight)) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveEnd", keyboardHeight, 1.0, @@ -372,6 +350,20 @@ class KeyboardAnimationCallback( return params } + private fun dispatchEventToJS(event: KeyboardTransitionEventData) { + if (event != lastEventDispatched) { + lastEventDispatched = event + context.dispatchEvent( + view.id, + KeyboardTransitionEvent( + surfaceId, + view.id, + data = event, + ), + ) + } + } + companion object { private const val DEFAULT_ANIMATION_TIME = 250 } diff --git a/example/src/screens/Examples/InteractiveKeyboard/index.tsx b/example/src/screens/Examples/InteractiveKeyboard/index.tsx index 3332cf43a9..49fa471931 100644 --- a/example/src/screens/Examples/InteractiveKeyboard/index.tsx +++ b/example/src/screens/Examples/InteractiveKeyboard/index.tsx @@ -6,6 +6,9 @@ import { useKeyboardHandler, } from 'react-native-keyboard-controller'; import Reanimated, { + scrollTo, + useAnimatedRef, + useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; @@ -17,7 +20,7 @@ import styles from './styles'; const AnimatedTextInput = Reanimated.createAnimatedComponent(TextInput); -const useKeyboardAnimation = () => { +const useKeyboardAnimation = ({ref, scroll}) => { const progress = useSharedValue(0); const height = useSharedValue(0); useKeyboardHandler({ @@ -30,6 +33,8 @@ const useKeyboardAnimation = () => { onInteractive: (e) => { 'worklet'; + scrollTo(ref, 0, scroll.value, false); + progress.value = e.progress; height.value = e.height; }, @@ -41,8 +46,16 @@ const useKeyboardAnimation = () => { type Props = StackScreenProps; function InteractiveKeyboard({ navigation }: Props) { + const aRef = useAnimatedRef(); + const scroll = useSharedValue(0); const [interpolator, setInterpolator] = useState<'ios' | 'linear'>('linear'); - const { height } = useKeyboardAnimation(); + const { height } = useKeyboardAnimation({ref: aRef, scroll: scroll}); + + const onScroll = useAnimatedScrollHandler({ + onScroll: (e) => { + scroll.value = e.contentOffset.y; + }, + }) useEffect(() => { navigation.setOptions({ @@ -76,7 +89,8 @@ function InteractiveKeyboard({ navigation }: Props) { ); const fakeView = useAnimatedStyle( () => ({ - height: height.value, + // TODO: don't update when onInteractive is fired + // height: height.value, }), [] ); @@ -89,6 +103,8 @@ function InteractiveKeyboard({ navigation }: Props) { showOnSwipeUp >