diff --git a/FabricExample/__tests__/focused-input.spec.tsx b/FabricExample/__tests__/focused-input.spec.tsx
new file mode 100644
index 0000000000..20beff6435
--- /dev/null
+++ b/FabricExample/__tests__/focused-input.spec.tsx
@@ -0,0 +1,62 @@
+import '@testing-library/jest-native/extend-expect';
+import React from 'react';
+import Reanimated, { useAnimatedStyle } from 'react-native-reanimated';
+import { render } from '@testing-library/react-native';
+
+import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';
+
+function RectangleWithFocusedInputLayout() {
+ const { input } = useReanimatedFocusedInput();
+ const style = useAnimatedStyle(
+ () => {
+ const layout = input.value?.layout;
+
+ return {
+ top: layout?.y,
+ left: layout?.x,
+ height: layout?.height,
+ width: layout?.width,
+ };
+ },
+ []
+ );
+
+ return ;
+}
+
+describe('`useReanimatedFocusedInput` mocking', () => {
+ it('should have different styles depends on `useReanimatedFocusedInput`', () => {
+ const { getByTestId, update } = render();
+
+ expect(getByTestId('view')).toHaveStyle({
+ top: 0,
+ left: 0,
+ width: 200,
+ height: 40,
+ });
+
+ (useReanimatedFocusedInput as jest.Mock).mockReturnValue({
+ input: {
+ value: {
+ target: 2,
+ layout: {
+ x: 10,
+ y: 100,
+ width: 190,
+ height: 80,
+ absoluteX: 100,
+ absoluteY: 200,
+ },
+ },
+ }
+ });
+ update();
+
+ expect(getByTestId('view')).toHaveStyle({
+ top: 100,
+ left: 10,
+ width: 190,
+ height: 80,
+ });
+ });
+});
diff --git a/FabricExample/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx b/FabricExample/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
index 7d9785bec3..6019acf6ea 100644
--- a/FabricExample/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
+++ b/FabricExample/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
@@ -1,9 +1,10 @@
import React, { FC } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
+import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import Reanimated, {
- MeasuredDimensions,
interpolate,
scrollTo,
+ useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
@@ -11,12 +12,9 @@ import Reanimated, {
useWorkletCallback,
} from 'react-native-reanimated';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';
-import { AwareScrollViewProvider, useAwareScrollView } from './context';
const BOTTOM_OFFSET = 50;
-type KeyboardAwareScrollViewProps = ScrollViewProps;
-
/**
* Everything begins from `onStart` handler. This handler is called every time,
* when keyboard changes its size or when focused `TextInput` was changed. In
@@ -55,22 +53,21 @@ type KeyboardAwareScrollViewProps = ScrollViewProps;
* +============================+ +============================+ +=====================================+
*
*/
-const KeyboardAwareScrollView: FC = ({
+const KeyboardAwareScrollView: FC = ({
children,
...rest
}) => {
const scrollViewAnimatedRef = useAnimatedRef();
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
- const layout = useSharedValue(null);
- const fakeViewHeight = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const tag = useSharedValue(-1);
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
+ const { input } = useReanimatedFocusedInput();
+ const layout = useSharedValue(null);
const { height } = useWindowDimensions();
- const { measure } = useAwareScrollView();
const onScroll = useAnimatedScrollHandler(
{
@@ -84,10 +81,9 @@ const KeyboardAwareScrollView: FC = ({
/**
* Function that will scroll a ScrollView as keyboard gets moving
*/
- const maybeScroll = useWorkletCallback((e: number, animated = false) => {
- fakeViewHeight.value = e;
+ const maybeScroll = useWorkletCallback((e: number, animated: boolean = false) => {
const visibleRect = height - keyboardHeight.value;
- const point = (layout.value?.pageY || 0) + (layout.value?.height || 0);
+ const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);
if (visibleRect - point <= BOTTOM_OFFSET) {
const interpolatedScrollTo = interpolate(
@@ -98,7 +94,11 @@ const KeyboardAwareScrollView: FC = ({
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated);
+
+ return interpolatedScrollTo;
}
+
+ return 0;
}, []);
useSmoothKeyboardHandler(
@@ -110,6 +110,8 @@ const KeyboardAwareScrollView: FC = ({
keyboardHeight.value !== e.height && e.height > 0;
const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0;
const keyboardWillHide = e.height === 0;
+ const focusWasChanged = (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize;
+
if (keyboardWillChangeSize) {
initialKeyboardSize.value = keyboardHeight.value;
}
@@ -120,7 +122,7 @@ const KeyboardAwareScrollView: FC = ({
scrollPosition.value = scrollBeforeKeyboardMovement.value;
}
- if (keyboardWillAppear || keyboardWillChangeSize) {
+ if (keyboardWillAppear || keyboardWillChangeSize || focusWasChanged) {
// persist scroll value
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
@@ -128,16 +130,20 @@ const KeyboardAwareScrollView: FC = ({
}
// focus was changed
- if (tag.value !== e.target || keyboardWillChangeSize) {
+ if (focusWasChanged) {
tag.value = e.target;
- if (tag.value !== -1) {
- // save position of focused text input when keyboard starts to move
- layout.value = measure(e.target);
- // save current scroll position - when keyboard will hide we'll reuse
- // this value to achieve smooth hide effect
- scrollBeforeKeyboardMovement.value = position.value;
- }
+ // save position of focused text input when keyboard starts to move
+ layout.value = input.value;
+ // save current scroll position - when keyboard will hide we'll reuse
+ // this value to achieve smooth hide effect
+ scrollBeforeKeyboardMovement.value = position.value;
+ }
+
+ if (focusWasChanged && !keyboardWillAppear) {
+ // update position on scroll value, so `onEnd` handler
+ // will pick up correct values
+ position.value += maybeScroll(e.height, true);
}
},
onMove: (e) => {
@@ -150,24 +156,24 @@ const KeyboardAwareScrollView: FC = ({
keyboardHeight.value = e.height;
scrollPosition.value = position.value;
-
- if (e.target !== -1 && e.height !== 0) {
- const prevLayout = layout.value;
- // just be sure, that view is no overlapped (i.e. focus changed)
- layout.value = measure(e.target);
- maybeScroll(e.height, true);
- // do layout substitution back to assure there will be correct
- // back transition when keyboard hides
- layout.value = prevLayout;
- }
},
},
[height]
);
+ useAnimatedReaction(() => input.value, (current, previous) => {
+ if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) {
+ const prevLayout = layout.value;
+
+ layout.value = input.value;
+ scrollPosition.value += maybeScroll(keyboardHeight.value, true);
+ layout.value = prevLayout;
+ }
+ }, []);
+
const view = useAnimatedStyle(
() => ({
- height: fakeViewHeight.value,
+ paddingBottom: keyboardHeight.value,
}),
[]
);
@@ -179,16 +185,11 @@ const KeyboardAwareScrollView: FC = ({
onScroll={onScroll}
scrollEventThrottle={16}
>
- {children}
-
+
+ {children}
+
);
};
-export default function (props: KeyboardAwareScrollViewProps) {
- return (
-
-
-
- );
-}
+export default KeyboardAwareScrollView;
diff --git a/FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx b/FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
index ecbcba1f0b..00846b8f81 100644
--- a/FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
+++ b/FabricExample/src/screens/Examples/AwareScrollView/TextInput.tsx
@@ -1,29 +1,32 @@
import React from 'react';
-import { TextInputProps, TextInput as TextInputRN } from 'react-native';
-import { randomColor } from '../../../utils';
-import { useAwareScrollView } from './context';
-
-const TextInput = React.forwardRef((props: TextInputProps, forwardRef) => {
- const { onRef } = useAwareScrollView();
+import { StyleSheet, TextInputProps, TextInput as TextInputRN } from 'react-native';
+const TextInput = (props: TextInputProps) => {
return (
{
- onRef(ref);
- if (typeof forwardRef === 'function') {
- forwardRef(ref);
- }
- }}
- placeholderTextColor="black"
- style={{
- width: '100%',
- height: 50,
- backgroundColor: randomColor(),
- marginTop: 50,
- }}
+ placeholderTextColor="#6c6c6c"
+ style={styles.container}
+ multiline
+ numberOfLines={10}
{...props}
+ placeholder={`${props.placeholder} (${props.keyboardType === 'default' ? 'text' : 'numeric'})`}
/>
);
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ minHeight: 50,
+ maxHeight: 200,
+ marginBottom: 50,
+ borderColor: 'black',
+ borderWidth: 2,
+ marginRight: 160,
+ borderRadius: 10,
+ color: 'black',
+ paddingHorizontal: 12,
+ },
});
export default TextInput;
diff --git a/FabricExample/src/screens/Examples/AwareScrollView/context.tsx b/FabricExample/src/screens/Examples/AwareScrollView/context.tsx
deleted file mode 100644
index f26e8c9872..0000000000
--- a/FabricExample/src/screens/Examples/AwareScrollView/context.tsx
+++ /dev/null
@@ -1,102 +0,0 @@
-declare const _IS_FABRIC: boolean;
-import React, {
- Component,
- RefObject,
- useContext,
- useMemo,
- useRef,
-} from 'react';
-import { TextInput, findNodeHandle } from 'react-native';
-import {
- useWorkletCallback,
- measure as measureREA,
- useSharedValue,
-} from 'react-native-reanimated';
-import { useHeaderHeight } from '@react-navigation/elements';
-
-type KeyboardAwareContext = {
- handlersRef: {
- current: Record>;
- };
- handlers: {
- value: Record>;
- };
-};
-
-const defaultValue: KeyboardAwareContext = {
- handlersRef: { current: {} },
- handlers: { value: {} },
-};
-
-const AwareScrollViewContext = React.createContext(defaultValue);
-
-export const useAwareScrollView = () => {
- const ctx = useContext(AwareScrollViewContext);
- const headerHeight = useHeaderHeight();
-
- const onRef = (ref: TextInput | null) => {
- const viewTag = findNodeHandle(ref);
- if (viewTag) {
- const viewTagOrShadowNode = _IS_FABRIC
- ? // @ts-expect-error this API doesn't have any types
- ref._internalInstanceHandle.stateNode.node
- : viewTag;
- ctx.handlersRef.current = {
- ...ctx.handlersRef.current,
- [viewTag]: () => {
- 'worklet';
-
- return viewTagOrShadowNode;
- },
- };
- ctx.handlers.value = ctx.handlersRef.current;
- }
- };
-
- const measure = useWorkletCallback(
- (tag: number) => {
- const ref = ctx.handlers.value[tag];
-
- if (ref) {
- const layout = measureREA(ref);
- if (layout) {
- return {
- ...layout,
- pageY: _IS_FABRIC ? layout.pageY + headerHeight : layout.pageY,
- };
- } else {
- return null;
- }
- } else {
- return null;
- }
- },
- [headerHeight]
- );
-
- return {
- onRef,
- measure,
- };
-};
-
-export const AwareScrollViewProvider: React.FC> = ({
- children,
-}) => {
- const handlersRef = useRef({});
- const handlers = useSharedValue({});
-
- const value = useMemo(
- () => ({
- handlersRef,
- handlers,
- }),
- []
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/FabricExample/src/screens/Examples/AwareScrollView/index.tsx b/FabricExample/src/screens/Examples/AwareScrollView/index.tsx
index 68af6e8ecd..482dc3feac 100644
--- a/FabricExample/src/screens/Examples/AwareScrollView/index.tsx
+++ b/FabricExample/src/screens/Examples/AwareScrollView/index.tsx
@@ -9,11 +9,11 @@ export default function AwareScrollView() {
useResizeMode();
return (
-
+
{new Array(10).fill(0).map((_, i) => (
))}
diff --git a/FabricExample/src/screens/Examples/AwareScrollView/styles.ts b/FabricExample/src/screens/Examples/AwareScrollView/styles.ts
index 31e937d84d..b84ce5c740 100644
--- a/FabricExample/src/screens/Examples/AwareScrollView/styles.ts
+++ b/FabricExample/src/screens/Examples/AwareScrollView/styles.ts
@@ -2,6 +2,9 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
- flex: 1,
+ paddingHorizontal: 16,
+ },
+ content: {
+ paddingTop: 50,
},
});
diff --git a/README.md b/README.md
index e22fa92f26..1b00a4f31b 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ Keyboard manager which works in identical way on both iOS and Android.
- reanimated support 🚀
- interactive keyboard dismissing 👆📱
- re-worked prebuilt components (such as `KeyboardAvoidingView`) 📚
+- easy focused input information retrieval 📝 🔮
- works with any navigation library 🧭
- and more is coming... Stay tuned! 😊
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt b/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt
new file mode 100644
index 0000000000..7be23b5a95
--- /dev/null
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/events/FocusedInputLayoutChangedEvent.kt
@@ -0,0 +1,41 @@
+package com.reactnativekeyboardcontroller.events
+
+import com.facebook.react.bridge.Arguments
+import com.facebook.react.bridge.WritableMap
+import com.facebook.react.uimanager.events.Event
+
+data class FocusedInputLayoutChangedEventData(
+ val x: Double,
+ val y: Double,
+ val width: Double,
+ val height: Double,
+ val absoluteX: Double,
+ val absoluteY: Double,
+ val target: Int,
+)
+
+class FocusedInputLayoutChangedEvent(
+ surfaceId: Int,
+ viewId: Int,
+ private val event: FocusedInputLayoutChangedEventData,
+) : Event(surfaceId, viewId) {
+ override fun getEventName() = "topFocusedInputLayoutChanged"
+
+ // All events for a given view can be coalesced
+ override fun getCoalescingKey(): Short = 0
+
+ override fun getEventData(): WritableMap? = Arguments.createMap().apply {
+ putInt("target", event.target)
+ putMap(
+ "layout",
+ Arguments.createMap().apply {
+ putDouble("x", event.x)
+ putDouble("y", event.y)
+ putDouble("width", event.width)
+ putDouble("height", event.height)
+ putDouble("absoluteX", event.absoluteX)
+ putDouble("absoluteY", event.absoluteY)
+ },
+ )
+ }
+}
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
index 46dde68448..8c0b188827 100644
--- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/ThemedReactContext.kt
@@ -2,6 +2,15 @@ package com.reactnativekeyboardcontroller.extensions
import android.view.View
import com.facebook.react.uimanager.ThemedReactContext
+import com.facebook.react.uimanager.UIManagerHelper
+import com.facebook.react.uimanager.events.Event
+import com.facebook.react.uimanager.events.EventDispatcher
val ThemedReactContext.rootView: View?
get() = this.currentActivity?.window?.decorView?.rootView
+
+fun ThemedReactContext?.dispatchEvent(viewId: Int, event: Event<*>) {
+ val eventDispatcher: EventDispatcher? =
+ UIManagerHelper.getEventDispatcherForReactTag(this, viewId)
+ eventDispatcher?.dispatchEvent(event)
+}
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt
index 168f4263d6..dd777cba65 100644
--- a/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/extensions/View.kt
@@ -48,3 +48,10 @@ fun View.copyBoundsInWindow(rect: Rect) {
)
}
}
+
+val View.screenLocation get(): IntArray {
+ val point = IntArray(2)
+ getLocationOnScreen(point)
+
+ return point
+}
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt
new file mode 100644
index 0000000000..da957dfee1
--- /dev/null
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/FocusedInputLayoutObserver.kt
@@ -0,0 +1,93 @@
+package com.reactnativekeyboardcontroller.listeners
+
+import android.view.View.OnLayoutChangeListener
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
+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.events.FocusedInputLayoutChangedEvent
+import com.reactnativekeyboardcontroller.events.FocusedInputLayoutChangedEventData
+import com.reactnativekeyboardcontroller.extensions.dispatchEvent
+import com.reactnativekeyboardcontroller.extensions.dp
+import com.reactnativekeyboardcontroller.extensions.screenLocation
+
+val noFocusedInputEvent = FocusedInputLayoutChangedEventData(
+ x = 0.0,
+ y = 0.0,
+ width = 0.0,
+ height = 0.0,
+ absoluteX = 0.0,
+ absoluteY = 0.0,
+ target = -1,
+)
+
+class FocusedInputLayoutObserver(val view: ReactViewGroup, private val context: ThemedReactContext?) {
+ // constructor variables
+ private val surfaceId = UIManagerHelper.getSurfaceId(view)
+
+ // state variables
+ private var lastFocusedInput: ReactEditText? = null
+ private var lastEventDispatched: FocusedInputLayoutChangedEventData = noFocusedInputEvent
+
+ // listeners
+ private val layoutListener =
+ OnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
+ this.syncUpLayout()
+ }
+ private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
+ // unfocused or focused was changed
+ if (newFocus == null || oldFocus != null) {
+ lastFocusedInput?.removeOnLayoutChangeListener(layoutListener)
+ lastFocusedInput = null
+ }
+ if (newFocus is ReactEditText) {
+ lastFocusedInput = newFocus
+ newFocus.addOnLayoutChangeListener(layoutListener)
+ this.syncUpLayout()
+ }
+ // unfocused
+ if (newFocus == null) {
+ dispatchEventToJS(noFocusedInputEvent)
+ }
+ }
+
+ init {
+ view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
+ }
+
+ fun syncUpLayout() {
+ val input = lastFocusedInput ?: return
+
+ val (x, y) = input.screenLocation
+ val event = FocusedInputLayoutChangedEventData(
+ x = input.x.dp,
+ y = input.y.dp,
+ width = input.width.toFloat().dp,
+ height = input.height.toFloat().dp,
+ absoluteX = x.toFloat().dp,
+ absoluteY = y.toFloat().dp,
+ target = input.id,
+ )
+
+ dispatchEventToJS(event)
+ }
+
+ fun destroy() {
+ view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
+ }
+
+ private fun dispatchEventToJS(event: FocusedInputLayoutChangedEventData) {
+ if (event != lastEventDispatched) {
+ lastEventDispatched = event
+ context.dispatchEvent(
+ view.id,
+ FocusedInputLayoutChangedEvent(
+ surfaceId,
+ view.id,
+ event = event,
+ ),
+ )
+ }
+ }
+}
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt
similarity index 76%
rename from android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt
rename to android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt
index 8fb01418d3..e94eb87132 100644
--- a/android/src/main/java/com/reactnativekeyboardcontroller/KeyboardAnimationCallback.kt
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/listeners/KeyboardAnimationCallback.kt
@@ -1,9 +1,10 @@
-package com.reactnativekeyboardcontroller
+package com.reactnativekeyboardcontroller.listeners
import android.animation.ValueAnimator
import android.os.Build
import android.util.Log
import android.view.View
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener
import androidx.core.animation.doOnEnd
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
@@ -15,11 +16,11 @@ 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
-import com.facebook.react.uimanager.events.EventDispatcher
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.extensions.dispatchEvent
import com.reactnativekeyboardcontroller.extensions.dp
import kotlin.math.abs
@@ -33,56 +34,65 @@ class KeyboardAnimationCallback(
val context: ThemedReactContext?,
) : WindowInsetsAnimationCompat.Callback(dispatchMode), OnApplyWindowInsetsListener {
private val surfaceId = UIManagerHelper.getSurfaceId(view)
+
+ // state variables
private var persistentKeyboardHeight = 0.0
private var isKeyboardVisible = false
private var isTransitioning = false
private var duration = 0
private var viewTagFocused = -1
+ // listeners
+ private val focusListener = OnGlobalFocusChangeListener { oldFocus, newFocus ->
+ if (newFocus is ReactEditText) {
+ viewTagFocused = newFocus.id
+
+ // keyboard is visible and focus has been changed
+ if (this.isKeyboardVisible && oldFocus !== null) {
+ // imitate iOS behavior and send two instant start/end events containing an info about new tag
+ // 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
+ // 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,
+ "topKeyboardMoveStart",
+ this.persistentKeyboardHeight,
+ 1.0,
+ 0,
+ viewTagFocused,
+ ),
+ )
+ context.dispatchEvent(
+ view.id,
+ KeyboardTransitionEvent(
+ surfaceId,
+ view.id,
+ "topKeyboardMoveEnd",
+ this.persistentKeyboardHeight,
+ 1.0,
+ 0,
+ viewTagFocused,
+ ),
+ )
+ this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
+ this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
+ }
+ }
+ }
+ private var layoutObserver: FocusedInputLayoutObserver? = null
+
init {
require(persistentInsetTypes and deferredInsetTypes == 0) {
"persistentInsetTypes and deferredInsetTypes can not contain any of " +
" same WindowInsetsCompat.Type values"
}
- view.viewTreeObserver.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
- if (newFocus is ReactEditText) {
- viewTagFocused = newFocus.id
-
- // keyboard is visible and focus has been changed
- if (this.isKeyboardVisible && oldFocus !== null) {
- // imitate iOS behavior and send two instant start/end events containing an info about new tag
- // 1. onStart/onMove/onEnd can be still dispatched after, if keyboard change size (numeric -> alphabetic type)
- // 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
- this.sendEventToJS(
- KeyboardTransitionEvent(
- surfaceId,
- view.id,
- "topKeyboardMoveStart",
- this.persistentKeyboardHeight,
- 1.0,
- 0,
- viewTagFocused,
- ),
- )
- this.sendEventToJS(
- KeyboardTransitionEvent(
- surfaceId,
- view.id,
- "topKeyboardMoveEnd",
- this.persistentKeyboardHeight,
- 1.0,
- 0,
- viewTagFocused,
- ),
- )
- this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(this.persistentKeyboardHeight))
- this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(this.persistentKeyboardHeight))
- }
- }
- }
+ layoutObserver = FocusedInputLayoutObserver(view = view, context = context)
+ view.viewTreeObserver.addOnGlobalFocusChangeListener(focusListener)
}
/**
@@ -115,8 +125,10 @@ class KeyboardAnimationCallback(
val keyboardHeight = getCurrentKeyboardHeight()
val duration = DEFAULT_ANIMATION_TIME.toInt()
+ layoutObserver?.syncUpLayout()
this.emitEvent("KeyboardController::keyboardWillShow", getEventParams(keyboardHeight))
- this.sendEventToJS(
+ context.dispatchEvent(
+ view.id,
KeyboardTransitionEvent(
surfaceId,
view.id,
@@ -128,10 +140,12 @@ class KeyboardAnimationCallback(
),
)
- val animation = ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
+ val animation =
+ ValueAnimator.ofFloat(this.persistentKeyboardHeight.toFloat(), keyboardHeight.toFloat())
animation.addUpdateListener { animator ->
val toValue = animator.animatedValue as Float
- this.sendEventToJS(
+ context.dispatchEvent(
+ view.id,
KeyboardTransitionEvent(
surfaceId,
view.id,
@@ -145,7 +159,8 @@ class KeyboardAnimationCallback(
}
animation.doOnEnd {
this.emitEvent("KeyboardController::keyboardDidShow", getEventParams(keyboardHeight))
- this.sendEventToJS(
+ context.dispatchEvent(
+ view.id,
KeyboardTransitionEvent(
surfaceId,
view.id,
@@ -180,13 +195,15 @@ class KeyboardAnimationCallback(
this.persistentKeyboardHeight = keyboardHeight
}
+ layoutObserver?.syncUpLayout()
this.emitEvent(
"KeyboardController::" + if (!isKeyboardVisible) "keyboardWillHide" else "keyboardWillShow",
getEventParams(keyboardHeight),
)
Log.i(TAG, "HEIGHT:: $keyboardHeight TAG:: $viewTagFocused")
- this.sendEventToJS(
+ context.dispatchEvent(
+ view.id,
KeyboardTransitionEvent(
surfaceId,
view.id,
@@ -227,10 +244,24 @@ class KeyboardAnimationCallback(
// do nothing, just log an exception send progress as 0
Log.w(TAG, "Caught arithmetic exception during `progress` calculation: $e")
}
- Log.i(TAG, "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused")
+ Log.i(
+ TAG,
+ "DiffY: $diffY $height $progress ${InteractiveKeyboardProvider.isInteractive} $viewTagFocused",
+ )
val event = if (InteractiveKeyboardProvider.isInteractive) "topKeyboardMoveInteractive" else "topKeyboardMove"
- this.sendEventToJS(KeyboardTransitionEvent(surfaceId, view.id, event, height, progress, duration, viewTagFocused))
+ context.dispatchEvent(
+ view.id,
+ KeyboardTransitionEvent(
+ surfaceId,
+ view.id,
+ event,
+ height,
+ progress,
+ duration,
+ viewTagFocused,
+ ),
+ )
return insets
}
@@ -259,7 +290,8 @@ class KeyboardAnimationCallback(
"KeyboardController::" + if (!isKeyboardVisible) "keyboardDidHide" else "keyboardDidShow",
getEventParams(keyboardHeight),
)
- this.sendEventToJS(
+ context.dispatchEvent(
+ view.id,
KeyboardTransitionEvent(
surfaceId,
view.id,
@@ -275,6 +307,11 @@ class KeyboardAnimationCallback(
duration = 0
}
+ fun destroy() {
+ view.viewTreeObserver.removeOnGlobalFocusChangeListener(focusListener)
+ layoutObserver?.destroy()
+ }
+
private fun isKeyboardVisible(): Boolean {
val insets = ViewCompat.getRootWindowInsets(view)
@@ -290,12 +327,6 @@ class KeyboardAnimationCallback(
return (keyboardHeight - navigationBar).toFloat().dp.coerceAtLeast(0.0)
}
- private fun sendEventToJS(event: Event<*>) {
- val eventDispatcher: EventDispatcher? =
- UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
- eventDispatcher?.dispatchEvent(event)
- }
-
private fun emitEvent(event: String, params: WritableMap) {
context?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(event, params)
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt
index 29ea43c8e0..79c2ee7f64 100644
--- a/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/managers/KeyboardControllerViewManagerImpl.kt
@@ -33,6 +33,8 @@ class KeyboardControllerViewManagerImpl(mReactContext: ReactApplicationContext)
MapBuilder.of("registrationName", "onKeyboardMoveEnd"),
"topKeyboardMoveInteractive",
MapBuilder.of("registrationName", "onKeyboardMoveInteractive"),
+ "topFocusedInputLayoutChanged",
+ MapBuilder.of("registrationName", "onFocusedInputLayoutChanged"),
)
return map
diff --git a/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
index dd1a0f7fdc..e5f8892e8e 100644
--- a/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
+++ b/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt
@@ -12,10 +12,10 @@ import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.views.view.ReactViewGroup
-import com.reactnativekeyboardcontroller.KeyboardAnimationCallback
import com.reactnativekeyboardcontroller.extensions.removeSelf
import com.reactnativekeyboardcontroller.extensions.requestApplyInsetsWhenAttached
import com.reactnativekeyboardcontroller.extensions.rootView
+import com.reactnativekeyboardcontroller.listeners.KeyboardAnimationCallback
private val TAG = EdgeToEdgeReactViewGroup::class.qualifiedName
@@ -30,6 +30,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
// internal class members
private var eventView: ReactViewGroup? = null
private var wasMounted = false
+ private var callback: KeyboardAnimationCallback? = null
// region View lifecycles
override fun onAttachedToWindow() {
@@ -47,11 +48,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
- // we need to remove view asynchronously from `onDetachedFromWindow` method
- // otherwise we may face NPE when app is getting opened via universal link
- // see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
- // for more details
- Handler(Looper.getMainLooper()).post { this.removeKeyboardCallbacks() }
+ this.removeKeyboardCallbacks()
}
// endregion
@@ -118,7 +115,7 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
val root = this.getContentView()
root?.addView(eventView)
- val callback = KeyboardAnimationCallback(
+ callback = KeyboardAnimationCallback(
view = this,
persistentInsetTypes = WindowInsetsCompat.Type.systemBars(),
deferredInsetTypes = WindowInsetsCompat.Type.ime(),
@@ -137,7 +134,18 @@ class EdgeToEdgeReactViewGroup(private val reactContext: ThemedReactContext) : R
}
private fun removeKeyboardCallbacks() {
- eventView.removeSelf()
+ callback?.destroy()
+
+ // capture view into closure, because if `onDetachedFromWindow` and `onAttachedToWindow`
+ // dispatched synchronously after each other (open application on Fabric), then `.post`
+ // will destroy just newly created view (if we have a reference via `this`)
+ // and we'll have a memory leak or zombie-view
+ val view = eventView
+ // we need to remove view asynchronously from `onDetachedFromWindow` method
+ // otherwise we may face NPE when app is getting opened via universal link
+ // see https://github.com/kirillzyusko/react-native-keyboard-controller/issues/242
+ // for more details
+ Handler(Looper.getMainLooper()).post { view.removeSelf() }
}
private fun getContentView(): FitWindowsLinearLayout? {
diff --git a/docs/docs/api/hooks/input/_category_.json b/docs/docs/api/hooks/input/_category_.json
new file mode 100644
index 0000000000..3e58fb0f35
--- /dev/null
+++ b/docs/docs/api/hooks/input/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "📝 Input",
+ "position": 2
+}
diff --git a/docs/docs/api/hooks/input/use-reanimated-focused-input.md b/docs/docs/api/hooks/input/use-reanimated-focused-input.md
new file mode 100644
index 0000000000..12e6133f20
--- /dev/null
+++ b/docs/docs/api/hooks/input/use-reanimated-focused-input.md
@@ -0,0 +1,43 @@
+---
+keywords: [react-native-keyboard-controller, useReanimatedFocusedInput, react-native-reanimated, react hook, focused input, layout]
+---
+
+# useReanimatedFocusedInput
+
+Hook that returns an information about `TextInput` that currently has a focus. Returns `null` if no input has focus.
+
+Hook will update its value in next cases:
+
+- when keyboard changes its size (appears, disappears, changes size because of different input mode);
+- when focus was changed from one `TextInput` to another;
+- when `layout` of focused input was changed.
+
+:::info Events order
+The value from `useReanimatedFocusedInput` will be always updated before keyboard events, so you can safely read values in `onStart` handler and be sure they are up-to-date.
+:::
+
+## Event structure
+
+The `input` property from this hook is returned as `SharedValue`. The returned data has next structure:
+
+```ts
+type KeyboardEventData = {
+ target: number; // tag of the focused TextInput
+ layout: { // layout of the focused TextInput
+ x: number; // `x` coordinate inside the parent component
+ y: number; // `y` coordinate inside the parent component
+ width: number; // `width` of the TextInput
+ height: number; // `height` of the TextInput
+ absoluteX: number; // `x` coordinate on the screen
+ absoluteY: number; // `y` coordinate on the screen
+ };
+};
+```
+
+## Example
+
+```tsx
+const {input} = useReanimatedFocusedInput();
+```
+
+Also have a look on [example](https://github.com/kirillzyusko/react-native-keyboard-controller/tree/main/example) app for more comprehensive usage.
diff --git a/docs/docs/api/hooks/module/_category_.json b/docs/docs/api/hooks/module/_category_.json
index ec194f2900..cccd2b80c9 100644
--- a/docs/docs/api/hooks/module/_category_.json
+++ b/docs/docs/api/hooks/module/_category_.json
@@ -1,4 +1,4 @@
{
"label": "📂 Module",
- "position": 2
+ "position": 3
}
diff --git a/example/__tests__/focused-input.spec.tsx b/example/__tests__/focused-input.spec.tsx
new file mode 100644
index 0000000000..20beff6435
--- /dev/null
+++ b/example/__tests__/focused-input.spec.tsx
@@ -0,0 +1,62 @@
+import '@testing-library/jest-native/extend-expect';
+import React from 'react';
+import Reanimated, { useAnimatedStyle } from 'react-native-reanimated';
+import { render } from '@testing-library/react-native';
+
+import { useReanimatedFocusedInput } from 'react-native-keyboard-controller';
+
+function RectangleWithFocusedInputLayout() {
+ const { input } = useReanimatedFocusedInput();
+ const style = useAnimatedStyle(
+ () => {
+ const layout = input.value?.layout;
+
+ return {
+ top: layout?.y,
+ left: layout?.x,
+ height: layout?.height,
+ width: layout?.width,
+ };
+ },
+ []
+ );
+
+ return ;
+}
+
+describe('`useReanimatedFocusedInput` mocking', () => {
+ it('should have different styles depends on `useReanimatedFocusedInput`', () => {
+ const { getByTestId, update } = render();
+
+ expect(getByTestId('view')).toHaveStyle({
+ top: 0,
+ left: 0,
+ width: 200,
+ height: 40,
+ });
+
+ (useReanimatedFocusedInput as jest.Mock).mockReturnValue({
+ input: {
+ value: {
+ target: 2,
+ layout: {
+ x: 10,
+ y: 100,
+ width: 190,
+ height: 80,
+ absoluteX: 100,
+ absoluteY: 200,
+ },
+ },
+ }
+ });
+ update();
+
+ expect(getByTestId('view')).toHaveStyle({
+ top: 100,
+ left: 10,
+ width: 190,
+ height: 80,
+ });
+ });
+});
diff --git a/example/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx b/example/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
index b7bbea795f..6019acf6ea 100644
--- a/example/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
+++ b/example/src/screens/Examples/AwareScrollView/KeyboardAwareScrollView.tsx
@@ -1,17 +1,16 @@
-import React, { Component, FC } from 'react';
+import React, { FC } from 'react';
import { ScrollViewProps, useWindowDimensions } from 'react-native';
+import { FocusedInputLayoutChangedEvent, useReanimatedFocusedInput } from 'react-native-keyboard-controller';
import Reanimated, {
- MeasuredDimensions,
interpolate,
- measure,
scrollTo,
+ useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useAnimatedStyle,
useSharedValue,
useWorkletCallback,
} from 'react-native-reanimated';
-import { RefObjectFunction } from 'react-native-reanimated/lib/types/lib/reanimated2/hook/commonTypes';
import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler';
const BOTTOM_OFFSET = 50;
@@ -61,12 +60,12 @@ const KeyboardAwareScrollView: FC = ({
const scrollViewAnimatedRef = useAnimatedRef();
const scrollPosition = useSharedValue(0);
const position = useSharedValue(0);
- const layout = useSharedValue(null);
- const fakeViewHeight = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const tag = useSharedValue(-1);
const initialKeyboardSize = useSharedValue(0);
const scrollBeforeKeyboardMovement = useSharedValue(0);
+ const { input } = useReanimatedFocusedInput();
+ const layout = useSharedValue(null);
const { height } = useWindowDimensions();
@@ -78,20 +77,13 @@ const KeyboardAwareScrollView: FC = ({
},
[]
);
- const measureByTag = useWorkletCallback((viewTag: number) => {
- return measure(
- (() => viewTag) as unknown as RefObjectFunction>
- );
- }, []);
/**
* Function that will scroll a ScrollView as keyboard gets moving
*/
- const maybeScroll = useWorkletCallback((e: number, animated = false) => {
- fakeViewHeight.value = e;
-
+ const maybeScroll = useWorkletCallback((e: number, animated: boolean = false) => {
const visibleRect = height - keyboardHeight.value;
- const point = (layout.value?.pageY || 0) + (layout.value?.height || 0);
+ const point = (layout.value?.layout.absoluteY || 0) + (layout.value?.layout.height || 0);
if (visibleRect - point <= BOTTOM_OFFSET) {
const interpolatedScrollTo = interpolate(
@@ -102,7 +94,11 @@ const KeyboardAwareScrollView: FC = ({
const targetScrollY =
Math.max(interpolatedScrollTo, 0) + scrollPosition.value;
scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated);
+
+ return interpolatedScrollTo;
}
+
+ return 0;
}, []);
useSmoothKeyboardHandler(
@@ -114,6 +110,8 @@ const KeyboardAwareScrollView: FC = ({
keyboardHeight.value !== e.height && e.height > 0;
const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0;
const keyboardWillHide = e.height === 0;
+ const focusWasChanged = (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize;
+
if (keyboardWillChangeSize) {
initialKeyboardSize.value = keyboardHeight.value;
}
@@ -124,7 +122,7 @@ const KeyboardAwareScrollView: FC = ({
scrollPosition.value = scrollBeforeKeyboardMovement.value;
}
- if (keyboardWillAppear || keyboardWillChangeSize) {
+ if (keyboardWillAppear || keyboardWillChangeSize || focusWasChanged) {
// persist scroll value
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
@@ -132,16 +130,20 @@ const KeyboardAwareScrollView: FC = ({
}
// focus was changed
- if (tag.value !== e.target || keyboardWillChangeSize) {
+ if (focusWasChanged) {
tag.value = e.target;
- if (tag.value !== -1) {
- // save position of focused text input when keyboard starts to move
- layout.value = measureByTag(e.target);
- // save current scroll position - when keyboard will hide we'll reuse
- // this value to achieve smooth hide effect
- scrollBeforeKeyboardMovement.value = position.value;
- }
+ // save position of focused text input when keyboard starts to move
+ layout.value = input.value;
+ // save current scroll position - when keyboard will hide we'll reuse
+ // this value to achieve smooth hide effect
+ scrollBeforeKeyboardMovement.value = position.value;
+ }
+
+ if (focusWasChanged && !keyboardWillAppear) {
+ // update position on scroll value, so `onEnd` handler
+ // will pick up correct values
+ position.value += maybeScroll(e.height, true);
}
},
onMove: (e) => {
@@ -154,24 +156,24 @@ const KeyboardAwareScrollView: FC = ({
keyboardHeight.value = e.height;
scrollPosition.value = position.value;
-
- if (e.target !== -1 && e.height !== 0) {
- const prevLayout = layout.value;
- // just be sure, that view is no overlapped (i.e. focus changed)
- layout.value = measureByTag(e.target);
- maybeScroll(e.height, true);
- // do layout substitution back to assure there will be correct
- // back transition when keyboard hides
- layout.value = prevLayout;
- }
},
},
[height]
);
+ useAnimatedReaction(() => input.value, (current, previous) => {
+ if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) {
+ const prevLayout = layout.value;
+
+ layout.value = input.value;
+ scrollPosition.value += maybeScroll(keyboardHeight.value, true);
+ layout.value = prevLayout;
+ }
+ }, []);
+
const view = useAnimatedStyle(
() => ({
- height: fakeViewHeight.value,
+ paddingBottom: keyboardHeight.value,
}),
[]
);
@@ -183,8 +185,9 @@ const KeyboardAwareScrollView: FC = ({
onScroll={onScroll}
scrollEventThrottle={16}
>
- {children}
-
+
+ {children}
+
);
};
diff --git a/example/src/screens/Examples/AwareScrollView/TextInput.tsx b/example/src/screens/Examples/AwareScrollView/TextInput.tsx
new file mode 100644
index 0000000000..f9b92bf5a1
--- /dev/null
+++ b/example/src/screens/Examples/AwareScrollView/TextInput.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { StyleSheet, TextInputProps, TextInput as TextInputRN } from 'react-native';
+
+const TextInput = (props: TextInputProps) => {
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ minHeight: 50,
+ maxHeight: 200,
+ marginBottom: 50,
+ borderColor: 'black',
+ borderWidth: 2,
+ marginRight: 160,
+ borderRadius: 10,
+ color: 'black',
+ paddingHorizontal: 12,
+ },
+});
+
+export default TextInput;
diff --git a/example/src/screens/Examples/AwareScrollView/index.tsx b/example/src/screens/Examples/AwareScrollView/index.tsx
index d0e29ed1c9..482dc3feac 100644
--- a/example/src/screens/Examples/AwareScrollView/index.tsx
+++ b/example/src/screens/Examples/AwareScrollView/index.tsx
@@ -1,29 +1,20 @@
import React from 'react';
-import { TextInput } from 'react-native';
import { useResizeMode } from 'react-native-keyboard-controller';
-import { randomColor } from '../../../utils';
-
import KeyboardAwareScrollView from './KeyboardAwareScrollView';
+import TextInput from './TextInput';
import { styles } from './styles';
export default function AwareScrollView() {
useResizeMode();
return (
-
+
{new Array(10).fill(0).map((_, i) => (
))}
diff --git a/example/src/screens/Examples/AwareScrollView/styles.ts b/example/src/screens/Examples/AwareScrollView/styles.ts
index 31e937d84d..b84ce5c740 100644
--- a/example/src/screens/Examples/AwareScrollView/styles.ts
+++ b/example/src/screens/Examples/AwareScrollView/styles.ts
@@ -2,6 +2,9 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
- flex: 1,
+ paddingHorizontal: 16,
+ },
+ content: {
+ paddingTop: 50,
},
});
diff --git a/ios/Extensions.swift b/ios/Extensions.swift
index d32a82d432..fbb3be2220 100644
--- a/ios/Extensions.swift
+++ b/ios/Extensions.swift
@@ -52,3 +52,10 @@ public extension Optional where Wrapped == UIResponder {
#endif
}
}
+
+public extension UIView {
+ var globalFrame: CGRect? {
+ let rootView = UIApplication.shared.keyWindow?.rootViewController?.view
+ return superview?.convert(frame, to: rootView)
+ }
+}
diff --git a/ios/KeyboardController.xcodeproj/project.pbxproj b/ios/KeyboardController.xcodeproj/project.pbxproj
index f847a691a9..cb50161f24 100644
--- a/ios/KeyboardController.xcodeproj/project.pbxproj
+++ b/ios/KeyboardController.xcodeproj/project.pbxproj
@@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
0807071E2A34807B00C05A19 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0807071D2A34807B00C05A19 /* Extensions.swift */; };
+ 084AEEC62ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */; };
+ 084AEEC82ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */; };
F333F8D428996B8D0015B05F /* KeyboardControllerView.mm in Sources */ = {isa = PBXBuildFile; fileRef = F333F8D228996B8D0015B05F /* KeyboardControllerView.mm */; };
F359D34F28133C26000B6AFE /* KeyboardControllerModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = F359D34E28133C26000B6AFE /* KeyboardControllerModule.mm */; };
F3626A0728B3FE760021B2D9 /* KeyboardMovementObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3626A0628B3FE760021B2D9 /* KeyboardMovementObserver.swift */; };
@@ -29,6 +31,9 @@
/* Begin PBXFileReference section */
0807071D2A34807B00C05A19 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; };
+ 084AEEC22ACF479A001A3069 /* FocusedInputLayoutChangedEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FocusedInputLayoutChangedEvent.h; sourceTree = ""; };
+ 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FocusedInputLayoutChangedEvent.m; sourceTree = ""; };
+ 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedInputLayoutObserver.swift; sourceTree = ""; };
134814201AA4EA6300B7C361 /* libKeyboardController.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libKeyboardController.a; sourceTree = BUILT_PRODUCTS_DIR; };
B3E7B5891CC2AC0600A0062D /* KeyboardControllerViewManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyboardControllerViewManager.mm; sourceTree = ""; };
F333F8D128996B1C0015B05F /* KeyboardControllerView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardControllerView.h; sourceTree = ""; };
@@ -58,6 +63,8 @@
children = (
F3F50667289E653B003091D6 /* KeyboardMoveEvent.h */,
F3F50668289E653B003091D6 /* KeyboardMoveEvent.m */,
+ 084AEEC22ACF479A001A3069 /* FocusedInputLayoutChangedEvent.h */,
+ 084AEEC52ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m */,
);
path = events;
sourceTree = "";
@@ -66,6 +73,7 @@
isa = PBXGroup;
children = (
F3626A0628B3FE760021B2D9 /* KeyboardMovementObserver.swift */,
+ 084AEEC72ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift */,
);
path = observers;
sourceTree = "";
@@ -160,9 +168,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 084AEEC62ACF49A8001A3069 /* FocusedInputLayoutChangedEvent.m in Sources */,
F3626A0728B3FE760021B2D9 /* KeyboardMovementObserver.swift in Sources */,
0807071E2A34807B00C05A19 /* Extensions.swift in Sources */,
F359D34F28133C26000B6AFE /* KeyboardControllerModule.mm in Sources */,
+ 084AEEC82ACF4AB2001A3069 /* FocusedInputLayoutObserver.swift in Sources */,
F4FF95D7245B92E800C19C63 /* KeyboardControllerViewManager.swift in Sources */,
F333F8D428996B8D0015B05F /* KeyboardControllerView.mm in Sources */,
F3F50669289E653B003091D6 /* KeyboardMoveEvent.m in Sources */,
diff --git a/ios/events/FocusedInputLayoutChangedEvent.h b/ios/events/FocusedInputLayoutChangedEvent.h
new file mode 100644
index 0000000000..c014975717
--- /dev/null
+++ b/ios/events/FocusedInputLayoutChangedEvent.h
@@ -0,0 +1,16 @@
+//
+// FocusedInputLayoutChangedEvent.h
+// KeyboardController
+//
+// Created by Kiryl Ziusko on 05/10/2023.
+// Copyright © 2023 Facebook. All rights reserved.
+//
+
+#import
+#import
+
+@interface FocusedInputLayoutChangedEvent : NSObject
+
+- (instancetype)initWithReactTag:(NSNumber *)reactTag event:(NSObject *)event;
+
+@end
diff --git a/ios/events/FocusedInputLayoutChangedEvent.m b/ios/events/FocusedInputLayoutChangedEvent.m
new file mode 100644
index 0000000000..69b1054ac0
--- /dev/null
+++ b/ios/events/FocusedInputLayoutChangedEvent.m
@@ -0,0 +1,75 @@
+//
+// FocusedInputLayoutChangedEvent.m
+// KeyboardController
+//
+// Created by Kiryl Ziusko on 05/10/2023.
+// Copyright © 2023 Facebook. All rights reserved.
+//
+
+#import "FocusedInputLayoutChangedEvent.h"
+#import
+
+@implementation FocusedInputLayoutChangedEvent {
+ NSNumber *_target;
+ NSObject *_layout;
+ uint16_t _coalescingKey;
+}
+
+- (NSString *)eventName
+{
+ return @"onFocusedInputLayoutChanged";
+}
+
+@synthesize viewTag = _viewTag;
+
+- (instancetype)initWithReactTag:(NSNumber *)reactTag event:(NSObject *)event
+{
+ RCTAssertParam(reactTag);
+
+ if ((self = [super init])) {
+ _viewTag = reactTag;
+ _target = [event valueForKey:@"target"];
+ _layout = [event valueForKey:@"layout"];
+ _coalescingKey = 0;
+ }
+ return self;
+}
+
+RCT_NOT_IMPLEMENTED(-(instancetype)init)
+
+- (uint16_t)coalescingKey
+{
+ return _coalescingKey;
+}
+
+- (NSDictionary *)body
+{
+ NSDictionary *body = @{
+ @"target" : _target,
+ @"layout" : _layout,
+ };
+
+ return body;
+}
+
+- (BOOL)canCoalesce
+{
+ return NO;
+}
+
+- (FocusedInputLayoutChangedEvent *)coalesceWithEvent:(FocusedInputLayoutChangedEvent *)newEvent
+{
+ return newEvent;
+}
+
++ (NSString *)moduleDotMethod
+{
+ return @"RCTEventEmitter.receiveEvent";
+}
+
+- (NSArray *)arguments
+{
+ return @[ self.viewTag, RCTNormalizeInputEventName(self.eventName), [self body] ];
+}
+
+@end
diff --git a/ios/observers/FocusedInputLayoutObserver.swift b/ios/observers/FocusedInputLayoutObserver.swift
new file mode 100644
index 0000000000..fc4f3c3172
--- /dev/null
+++ b/ios/observers/FocusedInputLayoutObserver.swift
@@ -0,0 +1,136 @@
+//
+// FocusedInputLayoutObserver.swift
+// KeyboardController
+//
+// Created by Kiryl Ziusko on 05/10/2023.
+// Copyright © 2023 Facebook. All rights reserved.
+//
+
+import Foundation
+import UIKit
+
+let noFocusedInputEvent: [String: Any] = [
+ "target": -1,
+ "layout": [
+ "absoluteX": 0,
+ "absoluteY": 0,
+ "width": 0,
+ "height": 0,
+ "x": 0,
+ "y": 0,
+ ],
+]
+
+@objc(FocusedInputLayoutObserver)
+public class FocusedInputLayoutObserver: NSObject {
+ // class members
+ var onEvent: (NSDictionary) -> Void
+ // input tracking
+ private var currentInput: UIView?
+ private var hasKVObserver = false
+ private var lastEventDispatched: [AnyHashable: Any] = noFocusedInputEvent
+
+ @objc public init(
+ handler: @escaping (NSDictionary) -> Void
+ ) {
+ onEvent = handler
+ }
+
+ @objc public func mount() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(keyboardWillShow),
+ name: UIResponder.keyboardWillShowNotification,
+ object: nil
+ )
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(keyboardWillHide),
+ name: UIResponder.keyboardWillHideNotification,
+ object: nil
+ )
+ }
+
+ @objc public func unmount() {
+ // swiftlint:disable:next notification_center_detachment
+ NotificationCenter.default.removeObserver(self)
+ }
+
+ @objc func keyboardWillShow(_: Notification) {
+ removeKVObserver()
+ currentInput = (UIResponder.current as? UIView)?.superview as UIView?
+ setupKVObserver()
+ syncUpLayout()
+ }
+
+ @objc func keyboardWillHide(_: Notification) {
+ removeKVObserver()
+ dispatchEventToJS(data: noFocusedInputEvent)
+ }
+
+ @objc func syncUpLayout() {
+ let responder = UIResponder.current
+ let focusedInput = (responder as? UIView)?.superview
+ let globalFrame = focusedInput?.globalFrame
+
+ let data: [String: Any] = [
+ "target": responder.reactViewTag,
+ "layout": [
+ "absoluteX": globalFrame?.origin.x,
+ "absoluteY": globalFrame?.origin.y,
+ "width": focusedInput?.frame.width,
+ "height": focusedInput?.frame.height,
+ "x": focusedInput?.frame.origin.x,
+ "y": focusedInput?.frame.origin.y,
+ ],
+ ]
+
+ dispatchEventToJS(data: data)
+ }
+
+ private func dispatchEventToJS(data: [String: Any]) {
+ if NSDictionary(dictionary: data).isEqual(to: lastEventDispatched) {
+ return
+ }
+
+ lastEventDispatched = data
+ onEvent(data as NSDictionary)
+ }
+
+ private func setupKVObserver() {
+ if hasKVObserver {
+ return
+ }
+
+ if currentInput != nil {
+ hasKVObserver = true
+ currentInput?.addObserver(self, forKeyPath: "center", options: .new, context: nil)
+ }
+ }
+
+ private func removeKVObserver() {
+ if !hasKVObserver {
+ return
+ }
+
+ hasKVObserver = false
+ currentInput?.removeObserver(self, forKeyPath: "center", context: nil)
+ }
+
+ // swiftlint:disable:next block_based_kvo
+ @objc override public func observeValue(
+ forKeyPath keyPath: String?,
+ of object: Any?,
+ change _: [NSKeyValueChangeKey: Any]?,
+ context _: UnsafeMutableRawPointer?
+ ) {
+ // swiftlint:disable:next force_cast
+ if keyPath == "center", object as! NSObject == currentInput! {
+ // we need to read layout in next frame, otherwise we'll get old
+ // layout values
+ DispatchQueue.main.async {
+ self.syncUpLayout()
+ }
+ }
+ }
+}
diff --git a/ios/views/KeyboardControllerView.mm b/ios/views/KeyboardControllerView.mm
index a7c19c122d..1b767ab2d6 100644
--- a/ios/views/KeyboardControllerView.mm
+++ b/ios/views/KeyboardControllerView.mm
@@ -9,6 +9,7 @@
// This guard prevent the code from being compiled in the old architecture
#ifdef RCT_NEW_ARCH_ENABLED
#import "KeyboardControllerView.h"
+#import "FocusedInputLayoutChangedEvent.h"
#import "KeyboardMoveEvent.h"
#import "react_native_keyboard_controller-Swift.h"
@@ -29,7 +30,8 @@ @interface KeyboardControllerView ()
@end
@implementation KeyboardControllerView {
- KeyboardMovementObserver *observer;
+ KeyboardMovementObserver *keyboardObserver;
+ FocusedInputLayoutObserver *inputObserver;
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -43,7 +45,39 @@ - (instancetype)initWithFrame:(CGRect)frame
static const auto defaultProps = std::make_shared();
_props = defaultProps;
- observer = [[KeyboardMovementObserver alloc]
+ inputObserver = [[FocusedInputLayoutObserver alloc] initWithHandler:^(NSDictionary *event) {
+ if (self->_eventEmitter) {
+ int target = [event[@"target"] integerValue];
+ double absoluteY = [event[@"layout"][@"absoluteY"] doubleValue];
+ double absoulteX = [event[@"layout"][@"absoluteX"] doubleValue];
+ double y = [event[@"layout"][@"y"] doubleValue];
+ double x = [event[@"layout"][@"x"] doubleValue];
+ double width = [event[@"layout"][@"width"] doubleValue];
+ double height = [event[@"layout"][@"height"] doubleValue];
+
+ std::dynamic_pointer_cast(
+ self->_eventEmitter)
+ ->onFocusedInputLayoutChanged(
+ facebook::react::KeyboardControllerViewEventEmitter::OnFocusedInputLayoutChanged{
+ .target = target,
+ .layout = facebook::react::KeyboardControllerViewEventEmitter::
+ OnFocusedInputLayoutChangedLayout{
+ .absoluteY = absoluteY,
+ .absoluteX = absoulteX,
+ .height = height,
+ .width = width,
+ .x = x,
+ .y = y}});
+ // TODO: use built-in _eventEmitter once NativeAnimated module will use ModernEventemitter
+ RCTBridge *bridge = [RCTBridge currentBridge];
+ if (bridge && [bridge valueForKey:@"_jsThread"]) {
+ FocusedInputLayoutChangedEvent *inputChangedEvent =
+ [[FocusedInputLayoutChangedEvent alloc] initWithReactTag:@(self.tag) event:event];
+ [bridge.eventDispatcher sendEvent:inputChangedEvent];
+ }
+ }
+ }];
+ keyboardObserver = [[KeyboardMovementObserver alloc]
initWithHandler:^(
NSString *event,
NSNumber *height,
@@ -122,9 +156,11 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
if (newViewProps.enabled != oldViewProps.enabled) {
if (newViewProps.enabled) {
- [observer mount];
+ [inputObserver mount];
+ [keyboardObserver mount];
} else {
- [observer unmount];
+ [inputObserver unmount];
+ [keyboardObserver unmount];
}
}
diff --git a/ios/views/KeyboardControllerViewManager.mm b/ios/views/KeyboardControllerViewManager.mm
index 96a20ad7be..422ed3d001 100644
--- a/ios/views/KeyboardControllerViewManager.mm
+++ b/ios/views/KeyboardControllerViewManager.mm
@@ -6,9 +6,12 @@ @interface RCT_EXTERN_MODULE (KeyboardControllerViewManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL)
// callbacks
+/// keyboard callbacks
RCT_EXPORT_VIEW_PROPERTY(onKeyboardMoveStart, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onKeyboardMove, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onKeyboardMoveEnd, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onKeyboardMoveInteractive, RCTDirectEventBlock);
+/// input callbacks
+RCT_EXPORT_VIEW_PROPERTY(onFocusedInputLayoutChanged, RCTDirectEventBlock);
@end
diff --git a/ios/views/KeyboardControllerViewManager.swift b/ios/views/KeyboardControllerViewManager.swift
index 507357929f..3421a35a07 100644
--- a/ios/views/KeyboardControllerViewManager.swift
+++ b/ios/views/KeyboardControllerViewManager.swift
@@ -12,6 +12,7 @@ class KeyboardControllerViewManager: RCTViewManager {
class KeyboardControllerView: UIView {
// internal variables
private var keyboardObserver: KeyboardMovementObserver?
+ private var inputObserver: FocusedInputLayoutObserver?
private var eventDispatcher: RCTEventDispatcherProtocol
private var bridge: RCTBridge
// react callbacks
@@ -23,8 +24,10 @@ class KeyboardControllerView: UIView {
@objc var enabled: ObjCBool = true {
didSet {
if enabled.boolValue {
+ inputObserver?.mount()
keyboardObserver?.mount()
} else {
+ inputObserver?.unmount()
keyboardObserver?.unmount()
}
}
@@ -44,6 +47,7 @@ class KeyboardControllerView: UIView {
override func willMove(toWindow newWindow: UIWindow?) {
if newWindow == nil {
// Will be removed from a window
+ inputObserver?.unmount()
keyboardObserver?.unmount()
}
}
@@ -51,11 +55,21 @@ class KeyboardControllerView: UIView {
override func didMoveToWindow() {
if window != nil {
// Added to a window
+ inputObserver = FocusedInputLayoutObserver(handler: onInput)
+ inputObserver?.mount()
keyboardObserver = KeyboardMovementObserver(handler: onEvent, onNotify: onNotify)
keyboardObserver?.mount()
}
}
+ func onInput(event: NSObject) {
+ // we don't want to send event to JS before the JS thread is ready
+ if bridge.value(forKey: "_jsThread") == nil {
+ return
+ }
+ eventDispatcher.send(FocusedInputLayoutChangedEvent(reactTag: reactTag, event: event))
+ }
+
func onEvent(event: NSString, height: NSNumber, progress: NSNumber, duration: NSNumber, target: NSNumber) {
// we don't want to send event to JS before the JS thread is ready
if bridge.value(forKey: "_jsThread") == nil {
diff --git a/jest/index.js b/jest/index.js
index edd699b2d7..4b23331044 100644
--- a/jest/index.js
+++ b/jest/index.js
@@ -10,6 +10,21 @@ const values = {
height: { value: 0 },
},
};
+const focusedInput = {
+ input: {
+ value: {
+ target: 1,
+ layout: {
+ x: 0,
+ y: 0,
+ width: 200,
+ height: 40,
+ absoluteX: 0,
+ absoluteY: 100,
+ },
+ },
+ },
+};
const mock = {
// hooks
@@ -18,6 +33,7 @@ const mock = {
useResizeMode: jest.fn(),
useGenericKeyboardHandler: jest.fn(),
useKeyboardHandler: jest.fn(),
+ useReanimatedFocusedInput: jest.fn().mockReturnValue(focusedInput),
// modules
KeyboardController: {
setInputMode: jest.fn(),
diff --git a/package.json b/package.json
index c4ca59ed54..ceba5ee0e9 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"react-native",
"keyboard",
"animation",
+ "focused input",
"ios",
"android"
],
diff --git a/src/animated.tsx b/src/animated.tsx
index ca6190d85d..14f40a9c2e 100644
--- a/src/animated.tsx
+++ b/src/animated.tsx
@@ -6,10 +6,14 @@ import { KeyboardControllerView } from './bindings';
import { KeyboardContext } from './context';
import { useAnimatedValue, useSharedHandlers } from './internal';
import { applyMonkeyPatch, revertMonkeyPatch } from './monkey-patch';
-import { useAnimatedKeyboardHandler } from './reanimated';
+import {
+ useAnimatedKeyboardHandler,
+ useFocusedInputHandler,
+} from './reanimated';
import type { KeyboardAnimationContext } from './context';
import type {
+ FocusedInputLayoutChangedEvent,
KeyboardControllerProps,
KeyboardHandler,
NativeEvent,
@@ -79,6 +83,7 @@ export const KeyboardProvider = ({
// shared values
const progressSV = useSharedValue(0);
const heightSV = useSharedValue(0);
+ const layout = useSharedValue(null);
const { setHandlers, broadcast } = useSharedHandlers();
// memo
const context = useMemo(
@@ -86,6 +91,7 @@ export const KeyboardProvider = ({
enabled,
animated: { progress: progress, height: Animated.multiply(height, -1) },
reanimated: { progress: progressSV, height: heightSV },
+ layout,
setHandlers,
setEnabled,
}),
@@ -122,7 +128,7 @@ export const KeyboardProvider = ({
heightSV.value = -event.height;
}
};
- const handler = useAnimatedKeyboardHandler(
+ const keyboardHandler = useAnimatedKeyboardHandler(
{
onKeyboardMoveStart: (event: NativeEvent) => {
'worklet';
@@ -150,6 +156,20 @@ export const KeyboardProvider = ({
},
[]
);
+ const inputHandler = useFocusedInputHandler(
+ {
+ onFocusedInputLayoutChanged: (e) => {
+ 'worklet';
+
+ if (e.target !== -1) {
+ layout.value = e;
+ } else {
+ layout.value = null;
+ }
+ },
+ },
+ []
+ );
// effects
useEffect(() => {
if (enabled) {
@@ -163,10 +183,11 @@ export const KeyboardProvider = ({
;
setHandlers: (handlers: KeyboardHandlers) => void;
setEnabled: React.Dispatch>;
};
const NOOP = () => {};
-const DEFAULT_SHARED_VALUE: SharedValue = {
- value: 0,
+const withSharedValue = (value: T): SharedValue => ({
+ value,
addListener: NOOP,
removeListener: NOOP,
modify: NOOP,
-};
+});
+const DEFAULT_SHARED_VALUE = withSharedValue(0);
+const DEFAULT_LAYOUT = withSharedValue(
+ null
+);
const defaultContext: KeyboardAnimationContext = {
enabled: true,
animated: {
@@ -37,6 +42,7 @@ const defaultContext: KeyboardAnimationContext = {
progress: DEFAULT_SHARED_VALUE,
height: DEFAULT_SHARED_VALUE,
},
+ layout: DEFAULT_LAYOUT,
setHandlers: NOOP,
setEnabled: NOOP,
};
diff --git a/src/hooks.ts b/src/hooks.ts
index 97743ba3e1..43859e8394 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -63,3 +63,9 @@ export function useKeyboardController() {
return { setEnabled: context.setEnabled, enabled: context.enabled };
}
+
+export function useReanimatedFocusedInput() {
+ const context = useKeyboardContext();
+
+ return { input: context.layout };
+}
diff --git a/src/reanimated.native.ts b/src/reanimated.native.ts
index e46f9747fc..ea968083d7 100644
--- a/src/reanimated.native.ts
+++ b/src/reanimated.native.ts
@@ -1,6 +1,12 @@
import { useEvent, useHandler } from 'react-native-reanimated';
-import type { EventWithName, KeyboardHandlerHook, NativeEvent } from './types';
+import type {
+ EventWithName,
+ FocusedInputHandlerHook,
+ FocusedInputLayoutChangedEvent,
+ KeyboardHandlerHook,
+ NativeEvent,
+} from './types';
export const useAnimatedKeyboardHandler: KeyboardHandlerHook<
Record,
@@ -49,3 +55,26 @@ export const useAnimatedKeyboardHandler: KeyboardHandlerHook<
doDependenciesDiffer
);
};
+
+export const useFocusedInputHandler: FocusedInputHandlerHook<
+ Record,
+ EventWithName
+> = (handlers, dependencies) => {
+ const { context, doDependenciesDiffer } = useHandler(handlers, dependencies);
+
+ return useEvent(
+ (event) => {
+ 'worklet';
+ const { onFocusedInputLayoutChanged } = handlers;
+
+ if (
+ onFocusedInputLayoutChanged &&
+ event.eventName.endsWith('onFocusedInputLayoutChanged')
+ ) {
+ onFocusedInputLayoutChanged(event, context);
+ }
+ },
+ ['onFocusedInputLayoutChanged'],
+ doDependenciesDiffer
+ );
+};
diff --git a/src/reanimated.ts b/src/reanimated.ts
index 6380bec17b..dc7e82beb9 100644
--- a/src/reanimated.ts
+++ b/src/reanimated.ts
@@ -1,6 +1,17 @@
-import type { EventWithName, KeyboardHandlerHook, NativeEvent } from './types';
+import type {
+ EventWithName,
+ FocusedInputHandlerHook,
+ FocusedInputLayoutChangedEvent,
+ KeyboardHandlerHook,
+ NativeEvent,
+} from './types';
+const NOOP = () => () => {};
export const useAnimatedKeyboardHandler: KeyboardHandlerHook<
Record,
EventWithName
-> = () => () => {};
+> = NOOP;
+export const useFocusedInputHandler: FocusedInputHandlerHook<
+ Record,
+ EventWithName
+> = NOOP;
diff --git a/src/specs/KeyboardControllerViewNativeComponent.ts b/src/specs/KeyboardControllerViewNativeComponent.ts
index 94c35bd8a9..71a45c3274 100644
--- a/src/specs/KeyboardControllerViewNativeComponent.ts
+++ b/src/specs/KeyboardControllerViewNativeComponent.ts
@@ -15,6 +15,18 @@ type KeyboardMoveEvent = Readonly<{
target: Int32;
}>;
+type FocusedInputLayoutChangedEvent = Readonly<{
+ target: Int32;
+ layout: {
+ x: Double;
+ y: Double;
+ width: Double;
+ height: Double;
+ absoluteX: Double;
+ absoluteY: Double;
+ };
+}>;
+
export interface NativeProps extends ViewProps {
// props
enabled?: boolean;
@@ -25,6 +37,7 @@ export interface NativeProps extends ViewProps {
onKeyboardMove?: DirectEventHandler;
onKeyboardMoveEnd?: DirectEventHandler;
onKeyboardMoveInteractive?: DirectEventHandler;
+ onFocusedInputLayoutChanged?: DirectEventHandler;
}
export default codegenNativeComponent(
diff --git a/src/types.ts b/src/types.ts
index ef49f3479b..3c58e8b509 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -11,6 +11,17 @@ export type NativeEvent = {
duration: number;
target: number;
};
+export type FocusedInputLayoutChangedEvent = {
+ target: number;
+ layout: {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ absoluteX: number;
+ absoluteY: number;
+ };
+};
export type EventWithName = {
eventName: string;
} & T;
@@ -30,10 +41,16 @@ export type KeyboardControllerProps = {
onKeyboardMoveInteractive?: (
e: NativeSyntheticEvent>
) => void;
- // fake prop used to activate reanimated bindings
+ onFocusedInputLayoutChanged?: (
+ e: NativeSyntheticEvent>
+ ) => void;
+ // fake props used to activate reanimated bindings
onKeyboardMoveReanimated?: (
e: NativeSyntheticEvent>
) => void;
+ onFocusedInputLayoutChangedReanimated?: (
+ e: NativeSyntheticEvent>
+ ) => void;
statusBarTranslucent?: boolean;
navigationBarTranslucent?: boolean;
} & ViewProps;
@@ -91,6 +108,15 @@ export type KeyboardHandlerHook = (
},
dependencies?: unknown[]
) => (e: NativeSyntheticEvent) => void;
+export type FocusedInputHandlerHook = (
+ handlers: {
+ onFocusedInputLayoutChanged?: (
+ e: FocusedInputLayoutChangedEvent,
+ context: TContext
+ ) => void;
+ },
+ dependencies?: unknown[]
+) => (e: NativeSyntheticEvent) => void;
// package types
export type Handlers = Record;