Skip to content
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

feat: new behavior="translate-with-padding" for KeyboardAvoidingView #830

Merged
merged 8 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions FabricExample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@react-native-community/blur": "^4.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/elements": "^2.2.5",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@react-navigation/stack": "^6.4.1",
Expand Down
50 changes: 10 additions & 40 deletions FabricExample/src/screens/Examples/ReanimatedChatFlatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { useHeaderHeight } from "@react-navigation/elements";
import React from "react";
import { FlatList, TextInput, View } from "react-native";
import { useKeyboardHandler } from "react-native-keyboard-controller";
import Animated, {
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { FlatList, TextInput } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";

import Message from "../../../components/Message";
import { history } from "../../../components/Message/data";
Expand All @@ -20,41 +17,15 @@ const RenderItem: ListRenderItem<MessageProps> = ({ item, index }) => {
return <Message key={index} {...item} />;
};

const useGradualAnimation = () => {
const height = useSharedValue(0);

useKeyboardHandler(
{
onMove: (e) => {
"worklet";

// eslint-disable-next-line react-compiler/react-compiler
height.value = e.height;
},
onEnd: (e) => {
"worklet";

height.value = e.height;
},
},
[],
);

return { height };
};

function ReanimatedChatFlatList() {
const { height } = useGradualAnimation();

const fakeView = useAnimatedStyle(
() => ({
height: Math.abs(height.value),
}),
[],
);
const headerHeight = useHeaderHeight();

return (
<View style={styles.container}>
<KeyboardAvoidingView
behavior="translate-with-padding"
keyboardVerticalOffset={headerHeight}
style={styles.container}
>
<FlatList
inverted
contentContainerStyle={styles.contentContainer}
Expand All @@ -63,8 +34,7 @@ function ReanimatedChatFlatList() {
renderItem={RenderItem}
/>
<TextInput style={styles.textInput} />
<Animated.View style={fakeView} />
</View>
</KeyboardAvoidingView>
);
}

Expand Down
7 changes: 7 additions & 0 deletions FabricExample/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,13 @@
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.31.tgz#28dd802a0787bb03fc0e5be296daf1804dbebbcf"
integrity sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==

"@react-navigation/elements@^2.2.5":
version "2.2.5"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.2.5.tgz#0e2ca76e2003e96b417a3d7c2829bf1afd69193f"
integrity sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==
dependencies:
color "^4.2.3"

"@react-navigation/native-stack@^6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.11.0.tgz#a33f92cbd55dfe28fb0ba67df99aaa95240eb87c"
Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/components/keyboard-avoiding-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Specify how to react to the presence of the keyboard. Could be one value of:
- `position`
- `padding`
- `height`
- `translate-with-padding`

### `contentContainerStyle`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Specify how to react to the presence of the keyboard. Could be one value of:
- `position`
- `padding`
- `height`
- `translate-with-padding`

### `contentContainerStyle`

Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@react-native-community/blur": "^4.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/elements": "^2.2.5",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@react-navigation/stack": "^6.4.1",
Expand Down
50 changes: 10 additions & 40 deletions example/src/screens/Examples/ReanimatedChatFlatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { useHeaderHeight } from "@react-navigation/elements";
import React from "react";
import { FlatList, TextInput, View } from "react-native";
import { useKeyboardHandler } from "react-native-keyboard-controller";
import Animated, {
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { FlatList, TextInput } from "react-native";
import { KeyboardAvoidingView } from "react-native-keyboard-controller";

import Message from "../../../components/Message";
import { history } from "../../../components/Message/data";
Expand All @@ -20,41 +17,15 @@ const RenderItem: ListRenderItem<MessageProps> = ({ item, index }) => {
return <Message key={index} {...item} />;
};

const useGradualAnimation = () => {
const height = useSharedValue(0);

useKeyboardHandler(
{
onMove: (e) => {
"worklet";

// eslint-disable-next-line react-compiler/react-compiler
height.value = e.height;
},
onEnd: (e) => {
"worklet";

height.value = e.height;
},
},
[],
);

return { height };
};

function ReanimatedChatFlatList() {
const { height } = useGradualAnimation();

const fakeView = useAnimatedStyle(
() => ({
height: Math.abs(height.value),
}),
[],
);
const headerHeight = useHeaderHeight();

return (
<View style={styles.container}>
<KeyboardAvoidingView
behavior="translate-with-padding"
keyboardVerticalOffset={headerHeight}
style={styles.container}
>
<FlatList
inverted
contentContainerStyle={styles.contentContainer}
Expand All @@ -63,8 +34,7 @@ function ReanimatedChatFlatList() {
renderItem={RenderItem}
/>
<TextInput style={styles.textInput} />
<Animated.View style={fakeView} />
</View>
</KeyboardAvoidingView>
);
}

Expand Down
7 changes: 7 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2367,6 +2367,13 @@
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.31.tgz#28dd802a0787bb03fc0e5be296daf1804dbebbcf"
integrity sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==

"@react-navigation/elements@^2.2.5":
version "2.2.5"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.2.5.tgz#0e2ca76e2003e96b417a3d7c2829bf1afd69193f"
integrity sha512-sDhE+W14P7MNWLMxXg1MEVXwkLUpMZJGflE6nQNzLmolJQIHgcia0Mrm8uRa3bQovhxYu1UzEojLZ+caoZt7Fg==
dependencies:
color "^4.2.3"

"@react-navigation/native-stack@^6.11.0":
version "6.11.0"
resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-6.11.0.tgz#a33f92cbd55dfe28fb0ba67df99aaa95240eb87c"
Expand Down
54 changes: 54 additions & 0 deletions src/components/KeyboardAvoidingView/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useState } from "react";
import { Platform } from "react-native";
import { useSharedValue } from "react-native-reanimated";

import { useKeyboardContext } from "../../context";
import { useKeyboardHandler } from "../../hooks";

const OS = Platform.OS;

export const useKeyboardAnimation = () => {
const { reanimated } = useKeyboardContext();

Expand Down Expand Up @@ -53,3 +56,54 @@ export const useKeyboardAnimation = () => {

return { height, progress, heightWhenOpened, isClosed };
};
export const useTranslateAnimation = () => {
const { reanimated } = useKeyboardContext();

// calculate it only once on mount, to avoid `SharedValue` reads during a render
const [initialProgress] = useState(() => reanimated.progress.value);

const padding = useSharedValue(initialProgress);
const translate = useSharedValue(0);

useKeyboardHandler(
{
onStart: (e) => {
"worklet";

if (e.height === 0) {
// eslint-disable-next-line react-compiler/react-compiler
padding.value = 0;
}
if (OS === "ios") {
translate.value = e.progress;
}
},
onMove: (e) => {
"worklet";

if (OS === "android") {
translate.value = e.progress;
}
},
onInteractive: (e) => {
"worklet";

padding.value = 0;

translate.value = e.progress;
},
onEnd: (e) => {
"worklet";

padding.value = e.progress;

if (OS === "android") {
translate.value = e.progress;
}
},
},
[],
);

return { translate, padding };
};
27 changes: 21 additions & 6 deletions src/components/KeyboardAvoidingView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Reanimated, {

import { useWindowDimensions } from "../../hooks";

import { useKeyboardAnimation } from "./hooks";
import { useKeyboardAnimation, useTranslateAnimation } from "./hooks";

import type { LayoutRectangle, ViewProps } from "react-native";

Expand Down Expand Up @@ -45,7 +45,7 @@ export type KeyboardAvoidingViewProps = KeyboardAvoidingViewBaseProps &
/**
* Specify how to react to the presence of the keyboard.
*/
behavior?: "height" | "padding";
behavior?: "height" | "padding" | "translate-with-padding";

/**
* `contentContainerStyle` is not allowed for these behaviors.
Expand Down Expand Up @@ -85,6 +85,7 @@ const KeyboardAvoidingView = forwardRef<
const initialFrame = useSharedValue<LayoutRectangle | null>(null);
const frame = useDerivedValue(() => initialFrame.value || defaultLayout);

const { translate, padding } = useTranslateAnimation();
const keyboard = useKeyboardAnimation();
const { height: screenHeight } = useWindowDimensions();

Expand All @@ -96,6 +97,14 @@ const KeyboardAvoidingView = forwardRef<

return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
}, [screenHeight, keyboardVerticalOffset]);
const interpolateToRelativeKeyboardHeight = useCallback(
(value: number) => {
"worklet";

return interpolate(value, [0, 1], [0, relativeKeyboardHeight()]);
},
[relativeKeyboardHeight],
);

const onLayoutWorklet = useCallback((layout: LayoutRectangle) => {
"worklet";
Expand All @@ -114,11 +123,11 @@ const KeyboardAvoidingView = forwardRef<
);

const animatedStyle = useAnimatedStyle(() => {
const bottom = interpolate(
const bottom = interpolateToRelativeKeyboardHeight(
keyboard.progress.value,
[0, 1],
[0, relativeKeyboardHeight()],
);
const translateY = interpolateToRelativeKeyboardHeight(translate.value);
const paddingBottom = interpolateToRelativeKeyboardHeight(padding.value);
const bottomHeight = enabled ? bottom : 0;

switch (behavior) {
Expand All @@ -138,10 +147,16 @@ const KeyboardAvoidingView = forwardRef<
case "padding":
return { paddingBottom: bottomHeight };

case "translate-with-padding":
return {
paddingTop: paddingBottom,
transform: [{ translateY: -translateY }],
};

default:
return {};
}
}, [behavior, enabled, relativeKeyboardHeight]);
}, [behavior, enabled, interpolateToRelativeKeyboardHeight]);
const isPositionBehavior = behavior === "position";
const containerStyle = isPositionBehavior ? contentContainerStyle : style;
const combinedStyles = useMemo(
Expand Down