If you are developing app in React Native for quite some time now, you probably noticed that one of the most hard to work with component is the KeyboardAvoidingView
.
Even the pros are guilty about this, here's a 1-on-1 talk of William Candillon and Jonny burger on the challenges on working with KeyboardAvoidingView
.
There are packages like react-native-keyboard-aware-scrollview (just in case you are not "aware") that automatically scrolls to the focused TextInput
component. But, in your use case, that is not enough. You probably want to push an element on top of the soft-keyboard or change style when it is visible or hidden.
Well, you've come to the right place! Today, I'll show you how to do it!
If you are new to
react-native-reanimated
v2, I recommend this amazing video!
I recommend to type the code instead of just copy and pasting, it will help you in learning. But if you are impatient, you can go ahead and check the final result and then copy and paste the example usage.
Let's get started!
Boilerplate
Copy and paste this boilerplate code to speed things up.
import {useCallback, useEffect} from 'react';
import {Keyboard, KeyboardEventListener, ScreenRect} from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
export const useKeyboardAvoiding = () => {
};
Animated shared values
We need a way to track the keyboard visibility state and coordinates, so we will add two reanimated shared values. Shared values are "shared" because they run on the UI and JS thread. If you are not familiar with this, RN docs has a nice overview on this topic.
keyboardVisible
, as the name implies, will hold the visibility state of the keyboard.
keyboardEndCoordinates
will hold the coordinates, like x
& y
axis, and height
after the keyboard is displayed or hidden.
Add these variables inside the hook:
// other codes from prev step
export const useKeyboardAvoiding = () => {
const keyboardVisible = useSharedValue(false);
const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);
};
Listening to keyboard events
Now, let's add the keyboardWillChangeFrame
and keyboardWillHide
listeners.
We will add them on mount, and remove the listeners on unmount using useEffect
and then wrap the callbacks with useCallback
.
Listening to keyboardWillChangeFrame
:
export const useKeyboardAvoiding = () => {
// other codes from prev step
const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
() => {},
[],
);
useEffect(() => {
const emitter = Keyboard.addListener(
'keyboardWillChangeFrame',
handleKeyboardWillChangeFrame,
);
return () => emitter.remove();
}, [handleKeyboardWillChangeFrame]);
};
Listening to keyboardWillHide
:
export const useKeyboardAvoiding = () => {
// other codes from prev step
const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
() => {},
[],
);
useEffect(() => {
const emitter = Keyboard.addListener(
'keyboardWillHide',
handleKeyboardWillHide,
);
return () => emitter.remove();
}, [handleKeyboardWillHide]);
};
Handling keyboard events
We will now store the keyboard state into the shared values.
In order to assign a value to the shared values, we will need to assign it to the value
property.
Modify the handleKeyboardWillChangeFrame
and handleKeyboardWillHide
callbacks:
const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
({endCoordinates}) => {
keyboardVisible.value = true;
keyboardEndCoordinates.value = endCoordinates;
},
[keyboardEndCoordinates.value, keyboardVisible.value],
);
const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
({endCoordinates}) => {
keyboardVisible.value = false;
keyboardEndCoordinates.value = endCoordinates;
},
[keyboardEndCoordinates.value, keyboardVisible.value],
);
Animating the style
And now, the most exciting part of the tutorial!
Let's create an animated style based on the shared values. To create an animated style in reanimated v2, we will use the useAnimatedStyle
hook. We will also use the withTiming
higher-order function to nicely animate the transition.
By default, we will animate the bottom
style, but we will implement a way to customize this later.
We will use 90% of the keyboard height. It seems to have an extra offset to the height that will cause the component being animated to be pushed further from the top of the keyboard.
// add this below the shared value declarations
// and before any of the callbacks
const animatedStyle = useAnimatedStyle(() => {
const kbHeight = keyboardEndCoordinates.value?.height ?? 0;
return {
bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
};
}, [keyboardEndCoordinates, keyboardVisible]);
This is now ready to be used. But, it's not very flexible because it is fixed to animate the bottom
style. The next step is to add a way to customize this.
Customizing the animated style
The final step is to allow the hook to accept a custom function that returns the custom style. We need to use a worklet
function in order for this to work.
A worklet
function is run on the UI thread by reanimated V2. We will need to add a 'worklet'
directive at the very top of the function block.
Let's modify the hook to accept a animatedStyleWorkletFn
function and then we will use it inside the useAnimatedStyle
hook
// imported files are here
export type KeyboardAvoidingStyleWorkletFn = (
endCoordinates: Animated.SharedValue<ScreenRect | null>,
isVisible: Animated.SharedValue<boolean>,
) => ReturnType<typeof useAnimatedStyle>;
export const useKeyboardAvoiding = (
animatedStyleWorkletFn?: KeyboardAvoidingStyleWorkletFn,
) => {
const keyboardVisible = useSharedValue(false);
const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);
const animatedStyle = useAnimatedStyle(() => {
const kbHeight = keyboardEndCoordinates.value?.height ?? 0;
return animatedStyleWorkletFn
? animatedStyleWorkletFn(keyboardEndCoordinates, keyboardVisible)
: {
bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
};
}, [animatedStyleWorkletFn, keyboardEndCoordinates, keyboardVisible]);
// callbacks are here
return {keyboardVisible, keyboardEndCoordinates, animatedStyle};
};
Final result
import React from 'react';
import {useCallback, useEffect} from 'react';
import {
Keyboard,
KeyboardEventListener,
ScreenRect,
StyleSheet,
TextInput,
View,
Button,
} from 'react-native';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
export type KeyboardAvoidingStyleWorkletFn = (
endCoordinates: Animated.SharedValue<ScreenRect | null>,
isVisible: Animated.SharedValue<boolean>,
) => ReturnType<typeof useAnimatedStyle>;
export const useKeyboardAvoiding = (
animatedStyleWorkletFn?: KeyboardAvoidingStyleWorkletFn,
) => {
const keyboardVisible = useSharedValue(false);
const keyboardEndCoordinates = useSharedValue<ScreenRect | null>(null);
const animatedStyle = useAnimatedStyle(() => {
const kbHeight = keyboardEndCoordinates.value?.height ?? 0;
return animatedStyleWorkletFn
? animatedStyleWorkletFn(keyboardEndCoordinates, keyboardVisible)
: {
bottom: withTiming(keyboardVisible.value ? kbHeight * 0.9 : 0),
};
}, [animatedStyleWorkletFn, keyboardEndCoordinates, keyboardVisible]);
const handleKeyboardWillChangeFrame = useCallback<KeyboardEventListener>(
({endCoordinates}) => {
keyboardVisible.value = true;
keyboardEndCoordinates.value = endCoordinates;
},
[keyboardEndCoordinates, keyboardVisible],
);
const handleKeyboardWillHide = useCallback<KeyboardEventListener>(
({endCoordinates}) => {
keyboardVisible.value = false;
keyboardEndCoordinates.value = endCoordinates;
},
[keyboardEndCoordinates, keyboardVisible],
);
useEffect(() => {
const emitter = Keyboard.addListener(
'keyboardWillChangeFrame',
handleKeyboardWillChangeFrame,
);
return () => emitter.remove();
}, [handleKeyboardWillChangeFrame]);
useEffect(() => {
const emitter = Keyboard.addListener(
'keyboardWillHide',
handleKeyboardWillHide,
);
return () => emitter.remove();
}, [handleKeyboardWillHide]);
return {keyboardVisible, keyboardEndCoordinates, animatedStyle};
};
export const DefaultStyle = () => {
const {animatedStyle} = useKeyboardAvoiding();
return (
<View style={styles.containerStyle}>
<TextInput multiline style={styles.input} />
<Animated.View style={animatedStyle}>
<Button title="Save" onPress={() => {}} />
</Animated.View>
</View>
);
};
export const CustomStyle = () => {
const {animatedStyle} = useKeyboardAvoiding((endCoords, isVisible) => {
'worklet';
const height = endCoords.value?.height ?? 0;
return {
marginBottom: withTiming(isVisible.value ? height * 0.7 : 0),
};
});
return (
<Animated.View style={[styles.containerStyle, animatedStyle]}>
<TextInput multiline style={styles.input} />
<Button title="Save" onPress={() => {}} />
</Animated.View>
);
};
const styles = StyleSheet.create({
containerStyle: {
flex: 1,
},
input: {
height: 500,
},
});
I hope you enjoyed this tutorial! This is my first tech blog so I apologize for the rough edges. If you have any tutorial suggestions, please comment down below. Thank you!
Top comments (1)
Thank you so much! It's been a while since you posted this, but it really helped me right now, so thanks again, man.
I hope you doing well :)