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/extensions/ThemedReactContext.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt index 8c0b188827..a4622f0372 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt @@ -1,6 +1,9 @@ package com.reactnativekeyboardcontroller.extensions +import android.util.Log import android.view.View +import com.facebook.react.bridge.WritableMap +import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.Event @@ -14,3 +17,9 @@ fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) { UIManagerHelper.getEventDispatcherForReactTag(this, viewId) eventDispatcher?.dispatchEvent(event) } + +fun ThemedReactContext?.emitEvent(event: String, params: WritableMap) { + this?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params) + + Log.i("ThemedReactContext", event) +} diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt index 7451cb5e56..80fc727028 100644 --- a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt +++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt @@ -13,15 +13,16 @@ import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.WritableMap -import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper 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 com.reactnativekeyboardcontroller.extensions.emitEvent import com.reactnativekeyboardcontroller.extensions.isKeyboardAnimation import kotlin.math.abs @@ -43,6 +44,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 -> @@ -56,11 +58,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, @@ -68,11 +67,8 @@ class KeyboardAnimationCallback( viewTagFocused, ), ) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveEnd", this.persistentKeyboardHeight, 1.0, @@ -80,8 +76,8 @@ class KeyboardAnimationCallback( viewTagFocused, ), ) - this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight)) - this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight)) + context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight)) + context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight)) } } } @@ -158,17 +154,14 @@ class KeyboardAnimationCallback( } layoutObserver?.syncUpLayout() - this.emitEvent( + context.emitEvent( "KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow", getEventParams(keyboardHeight), ) 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, @@ -215,11 +208,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, @@ -255,15 +245,12 @@ class KeyboardAnimationCallback( } isKeyboardVisible = isKeyboardVisible || isKeyboardShown - this.emitEvent( + context.emitEvent( "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, @@ -299,12 +286,9 @@ class KeyboardAnimationCallback( this.animation?.cancel() } - this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight)) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + context.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight)) + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveStart", keyboardHeight, 1.0, @@ -317,11 +301,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, @@ -331,12 +312,9 @@ class KeyboardAnimationCallback( ) } animation.doOnEnd { - this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight)) - context.dispatchEvent( - view.id, - KeyboardTransitionEvent( - surfaceId, - view.id, + context.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight)) + this.dispatchEventToJS( + KeyboardTransitionEventData( "topKeyboardMoveEnd", keyboardHeight, 1.0, @@ -368,12 +346,6 @@ class KeyboardAnimationCallback( return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0) } - private fun emitEvent(event: String, params: WritableMap) { - context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params) - - Log.i(TAG, event) - } - private fun getEventParams(height: Double): WritableMap { val params: WritableMap = Arguments.createMap() params.putDouble("height", height) @@ -384,6 +356,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 >