|
1 |
| -import React, {forwardRef, useCallback, useMemo, useState} from 'react'; |
2 |
| -import type {LayoutRectangle, View, ViewProps} from 'react-native'; |
3 |
| -import {useKeyboardContext, useKeyboardHandler} from 'react-native-keyboard-controller'; |
4 |
| -import Reanimated, {interpolate, runOnUI, useAnimatedStyle, useDerivedValue, useSharedValue} from 'react-native-reanimated'; |
5 |
| -import {useSafeAreaFrame} from 'react-native-safe-area-context'; |
6 |
| -import type {KeyboardAvoidingViewProps} from './types'; |
7 |
| - |
8 |
| -const useKeyboardAnimation = () => { |
9 |
| - const {reanimated} = useKeyboardContext(); |
10 |
| - |
11 |
| - // calculate it only once on mount, to avoid `SharedValue` reads during a render |
12 |
| - const [initialHeight] = useState(() => -reanimated.height.get()); |
13 |
| - const [initialProgress] = useState(() => reanimated.progress.get()); |
14 |
| - |
15 |
| - const heightWhenOpened = useSharedValue(initialHeight); |
16 |
| - const height = useSharedValue(initialHeight); |
17 |
| - const progress = useSharedValue(initialProgress); |
18 |
| - const isClosed = useSharedValue(initialProgress === 0); |
19 |
| - |
20 |
| - useKeyboardHandler( |
21 |
| - { |
22 |
| - onStart: (e) => { |
23 |
| - 'worklet'; |
24 |
| - |
25 |
| - progress.set(e.progress); |
26 |
| - height.set(e.height); |
27 |
| - |
28 |
| - if (e.height > 0) { |
29 |
| - isClosed.set(false); |
30 |
| - heightWhenOpened.set(e.height); |
31 |
| - } |
32 |
| - }, |
33 |
| - onEnd: (e) => { |
34 |
| - 'worklet'; |
35 |
| - |
36 |
| - isClosed.set(e.height === 0); |
37 |
| - height.set(e.height); |
38 |
| - progress.set(e.progress); |
39 |
| - }, |
40 |
| - }, |
41 |
| - [], |
42 |
| - ); |
43 |
| - |
44 |
| - return {height, progress, heightWhenOpened, isClosed}; |
45 |
| -}; |
46 |
| - |
47 |
| -const defaultLayout: LayoutRectangle = { |
48 |
| - x: 0, |
49 |
| - y: 0, |
50 |
| - width: 0, |
51 |
| - height: 0, |
52 |
| -}; |
53 |
| - |
54 |
| -/** |
55 |
| - * View that moves out of the way when the keyboard appears by automatically |
56 |
| - * adjusting its height, position, or bottom padding. |
57 |
| - * |
58 |
| - * This `KeyboardAvoidingView` acts as a backward compatible layer for the previous Android behavior (prior to edge-to-edge mode). |
59 |
| - * We can use `KeyboardAvoidingView` directly from the `react-native-keyboard-controller` package, but in this case animations are stuttering and it's better to handle as a separate task. |
| 1 | +/* |
| 2 | + * The KeyboardAvoidingView is only used on ios |
60 | 3 | */
|
61 |
| -const KeyboardAvoidingView = forwardRef<View, React.PropsWithChildren<KeyboardAvoidingViewProps>>( |
62 |
| - ({behavior, children, contentContainerStyle, enabled = true, keyboardVerticalOffset = 0, style, onLayout: onLayoutProps, ...props}, ref) => { |
63 |
| - const initialFrame = useSharedValue<LayoutRectangle | null>(null); |
64 |
| - const frame = useDerivedValue(() => initialFrame.get() ?? defaultLayout); |
65 |
| - |
66 |
| - const keyboard = useKeyboardAnimation(); |
67 |
| - const {height: screenHeight} = useSafeAreaFrame(); |
68 |
| - |
69 |
| - const relativeKeyboardHeight = useCallback(() => { |
70 |
| - 'worklet'; |
71 |
| - |
72 |
| - const keyboardY = screenHeight - keyboard.heightWhenOpened.get() - keyboardVerticalOffset; |
73 |
| - |
74 |
| - return Math.max(frame.get().y + frame.get().height - keyboardY, 0); |
75 |
| - }, [screenHeight, keyboard.heightWhenOpened, keyboardVerticalOffset, frame]); |
76 |
| - |
77 |
| - const onLayoutWorklet = useCallback( |
78 |
| - (layout: LayoutRectangle) => { |
79 |
| - 'worklet'; |
80 |
| - |
81 |
| - if (keyboard.isClosed.get() || initialFrame.get() === null) { |
82 |
| - initialFrame.set(layout); |
83 |
| - } |
84 |
| - }, |
85 |
| - [initialFrame, keyboard.isClosed], |
86 |
| - ); |
87 |
| - const onLayout = useCallback<NonNullable<ViewProps['onLayout']>>( |
88 |
| - (e) => { |
89 |
| - runOnUI(onLayoutWorklet)(e.nativeEvent.layout); |
90 |
| - onLayoutProps?.(e); |
91 |
| - }, |
92 |
| - [onLayoutProps, onLayoutWorklet], |
93 |
| - ); |
94 |
| - |
95 |
| - const animatedStyle = useAnimatedStyle(() => { |
96 |
| - const bottom = interpolate(keyboard.progress.get(), [0, 1], [0, relativeKeyboardHeight()]); |
97 |
| - const bottomHeight = enabled ? bottom : 0; |
98 |
| - |
99 |
| - switch (behavior) { |
100 |
| - case 'height': |
101 |
| - if (!keyboard.isClosed.get()) { |
102 |
| - return { |
103 |
| - height: frame.get().height - bottomHeight, |
104 |
| - flex: 0, |
105 |
| - }; |
106 |
| - } |
107 |
| - |
108 |
| - return {}; |
109 |
| - |
110 |
| - case 'padding': |
111 |
| - return {paddingBottom: bottomHeight}; |
| 4 | +import React from 'react'; |
| 5 | +import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native-keyboard-controller'; |
| 6 | +import type {KeyboardAvoidingViewProps} from './types'; |
112 | 7 |
|
113 |
| - default: |
114 |
| - return {}; |
115 |
| - } |
116 |
| - }, [behavior, enabled, relativeKeyboardHeight]); |
117 |
| - const combinedStyles = useMemo(() => [style, animatedStyle], [style, animatedStyle]); |
| 8 | +function KeyboardAvoidingView(props: KeyboardAvoidingViewProps) { |
| 9 | + // eslint-disable-next-line react/jsx-props-no-spreading |
| 10 | + return <KeyboardAvoidingViewComponent {...props} />; |
| 11 | +} |
118 | 12 |
|
119 |
| - return ( |
120 |
| - <Reanimated.View |
121 |
| - ref={ref} |
122 |
| - style={combinedStyles} |
123 |
| - onLayout={onLayout} |
124 |
| - // eslint-disable-next-line react/jsx-props-no-spreading |
125 |
| - {...props} |
126 |
| - > |
127 |
| - {children} |
128 |
| - </Reanimated.View> |
129 |
| - ); |
130 |
| - }, |
131 |
| -); |
| 13 | +KeyboardAvoidingView.displayName = 'KeyboardAvoidingView'; |
132 | 14 |
|
133 | 15 | export default KeyboardAvoidingView;
|
0 commit comments