This post is mirrored on my blog, chrisfrew.in
Back in the React Native World
I've been deep in the React Native world recently as I develop big features for InClub, an app that enables you to join and host private events!
Our latest sprint (for "v.1.3.0π₯³") was designed to culminate in a chat functionality. That's right - I built an entire chat function in a mobile app in 3 weeks. π This wasn't your grandma's chat either - the full feature set included profile picture uploading, routing push notifications to the correct chat window based on sender, and creation of new channels based on various events around the app. I know, I know, most dev consultants would need teams of people to get this done...
You know how many people they needed to get the chat working?! TEAMS!
Yeah, I'm pretty awesome. π
All bravado aside, I had multiple cases of imposter syndrome and existential crisis during the sprint. (Did you know, for example, that Flipper screws up file uploads in development mode on React Native projects, and that commenting out a single line fixes it?) <-- that little gem right there took me 2 weeks to figure out why file uploads wouldn't work on Android devices. π
Alright, enough joking - let's get into the technical info and code.
Pesky Android Keyboards
The most critical screen of an entire chat function, in my opinion, is the chat window itself. You want a clean text input and send button - luckily, we have fantastic apps to use as examples. In our case, we followed the styling and layout used in Whatsapp, Tinder, and Facebook Messenger. While iPhones of all shapes and sizes were working great with React Native's standard KeyboardAvoidingView
component, I quickly learned that Android devices didn't like to abide by the same rules.
Alarm bells started going off when I read something on Stack Overflow (no link, I couldn't find it again) that some phone manufacturers for Android don't expose the keyboard API at all! So it was clear that I couldn't reliably trust whatever info Android phones were sending to React Native's KeyboardAvoidingView
. It was time to hunt for a different solution.
I found this potential solution from John Tucker via codeburst.io, which takes the input location and keyboard height itself and adjusts the screen accordingly - but there were two things that I didn't like with that post. The first was that it was the old school style of class components. Second, it was utilizing deprecated methods of TextInputState.currentlyFocusedField()
and UIManager.measure()
.
Long story short, I converted the class component to a functional component with hooks and found a way to replace those two deprecated methods. TextInputState.currentlyFocusedField()
can be replaced with TextInputState.currentlyFocusedInput()
which returns not a number (like currentlyFocusedField()
), but a React ref
to the input itself. This actually makes things easier while at the same time eliminating the second deprecation of using UIManager.measure()
, since we are able to call the measure()
method directly on the ref
returned by currentlyFocusedInput()
.
In the end, we get a shiny new 2021-friendly, less than 100 line, deliciously clean keyboard shifting view component:
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Animated, Dimensions, Keyboard, KeyboardAvoidingView, StyleSheet, TextInput } from 'react-native';
import {useHeaderHeight} from '@react-navigation/elements';
import { useKeyboard } from '@react-native-community/hooks';
export default function KeyboardShift (props: PropsWithChildren<{}>) {
const [shift, setShift] = useState(new Animated.Value(0))
const keyboard = useKeyboard()
// On mount, add keyboard show and hide listeners
// On unmount, remove them
useEffect(() => {
Keyboard.addListener('keyboardDidShow', handleKeyboardDidShow);
Keyboard.addListener('keyboardDidHide', handleKeyboardDidHide);
return () => {
Keyboard.removeAllListeners('keyboardDidShow');
Keyboard.removeAllListeners('keyboardDidHide');
}
}, [])
const handleKeyboardDidShow = () => {
const { height: windowHeight } = Dimensions.get('window');
const keyboardHeight = keyboard.keyboardHeight;
const currentlyFocusedInputRef = TextInput.State.currentlyFocusedInput();
currentlyFocusedInputRef.measure((x, y, width, height, pageX, pageY) => {
const fieldHeight = height;
const fieldTop = pageY;
const gap = (windowHeight - keyboardHeight) - (fieldTop + fieldHeight);
if (gap >= 0) {
return;
}
Animated.timing(
shift,
{
toValue: gap,
duration: 1000,
useNativeDriver: true,
}
).start();
})
}
const handleKeyboardDidHide = () => {
Animated.timing(
shift,
{
toValue: 0,
duration: 1000,
useNativeDriver: true,
}
).start();
}
const { children } = props;
// Android: we need an animated view since the keyboard style can vary widely
// And React Native's KeyboardAvoidingView isn't always reliable
if (Platform.OS === 'android') {
return (
<Animated.View style={[styles.container, { transform: [{translateY: shift}] }]}>
{children}
</Animated.View>
);
}
// iOS: React Native's KeyboardAvoidingView with header offset and
// behavior 'padding' works fine on all ios devices (and keyboard types)
const headerHeight = useHeaderHeight();
return (
<KeyboardAvoidingView
keyboardVerticalOffset={headerHeight}
style={styles.container}
behavior={'padding'}>
{children}
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1
}
});
Simply wrap the components in your screen that need a keyboard with the <KeyboardShift>
component and enjoy the perfection:
export default function YourCoolKeyboardScreen () {
// Other logic, variables, etc.
return (
<KeyboardShift>
{/* Screen components */}
</KeyboardShift>
)
}
Dependencies and Notes
Note that this solution relies on two additional libraries, @react-navigation/elements
for the header height, and @react-native-community/hooks
for the keyboard height. A repeating theme I've found in KeyboardAvoidingView
issues is the presence of React Navigation in a React Native project - so if you are in fact not using react-navigation
in your app (in all reality, not many apps go without this dependency anymore in 2021) - you could try React Native's standard KeyboardAvoidingView
solution for both iOS and Android. Otherwise, I of course suggest my solution. π It works well and we are using it in production.
Thanks and Stay Tuned π»
I'll likely be posting plenty more about React Native in the coming weeks months and years as we continue to build out the InClub mobile app.
Oh yeah - I should also mention, I also built a nice scaling input for the chat - you know, one that can grow to multiple lines as you write - just like the big boys. But that post will be for another day π.
Cheers! π»
-Chris
Top comments (2)
Hi, Is there someway to make the andoird to have the same smooth animation effect like IOS, The android one just feels boxy.
Hi , It would be great if you published a library out of these codes.