KeyboardAvoidingView - consistent behavior on IOS and Android without any additional libraries

The way to have consistent behaviors of KeyboardAvoidingView across the platforms.

Author: Bartosz Dadok


The goal of this article is to explain how to accomplish consistent keyboard behavior when the user focuses on the input. In the video below, you can see the best results you can achieve using KeyboardAvoidingView provided by React Native on the left side. The right side of the video shows the difference after implementing CustomKeyboardAvoidingView.

The article explains differences between keyboard behaviors on Android and IOS when implementing CustomKeyboardAvoidingView which gives a much better user experience while opening the keyboard.

If you like the behavior on the right side more than on the left, this article is for you!

If you have ever tried to implement KeyboardAvoidingView in React Native you can see the difference between IOS and Android. The most common approach is to implement KeyboardAvoidingView this way:

   <KeyboardAvoidingView 
    behavior={Platform.OS === 'ios' ? 'padding' : undefined}
    keyboardVerticalOffset={60}
    style={styles.container}>
    {children}
   </KeyboardAvoidingView>

This code is used in a majority of mobile apps written in React Native. There is nothing wrong with the code above, but we can have a much better user experience using a different approach to keyboard avoding view.

The effect of this code is seen on the left side of the video. Let's see that again:

Android

KeyboardAvoidingView-Android

IOS

KeyboardAvoidingView-IOS

As you can see on Android, the content jumps up while opening the keyboard. When on IOS, the behavior is smooth and friendly.

After reading this article, you will accomplish the same behavior on iOS and Android and have complete control over the keyboard and view.

Since IOS is working smoothly, let's focus first on Android.

Android API and keyboard modes

Android API allows us to decide how a layout should behave when we focus the input and the keyboard is showing. We can control that using the attribute windowSoftInputMode. The attribute is located in AndroidManifest.xml.

<activity
    android:name=".YourActivity"
    android:windowSoftInputMode="adjustResize">
    <!-- Other activity attributes -->
</activity>

In Expo, we can control the attribute in the app.json file.

"expo": {
    "android": {
      "softwareKeyboardLayoutMode": "adjustResize",
    },
}

In React Native and Expo projects by default, the windowSoftInputMode attribute is set to adjustResize.

But apart from that, we can set it to a different value.

The windowSoftInputMode attributes:

  • adjustResize: The view gets smaller to make room for the keyboard.
KeyboardAvoidingView-adjustResize
  • adjustPan: The view moves up when the keyboard shows up.
KeyboardAvoidingView-adjustPan
  • adjustNothing: The window remains unchanged, without resizing or shifting. Nothing happen with the view. We cannot even see the input, the view stays where it was.
KeyboardAvoidingView-adjustNothing
  • adjustUnspecified: The system decides whether to resize or move the window when the keyboard pops up. There is no GIF attached because you never know which value will be there.

Android and IOS differences

First, let's start with the plain TextInput provided by React Native; as you can see we don’t use the KeyboardAvoidingView component here.

We have freshly installed ReactNative or Expo project (by default, on Android, windowSoftInputMode is adjustResize) and View, which contains only a TextInput.

We have this screen:

<View style={{ flex: 1, justifyContent: "flex-end", backgroundColor: 'rgba(0,0,0,0.5)' }}>
    <TextInput style={{  height: 60, padding: 7, backgroundColor: 'rgba(0,0,0,0.1)' }}/>
<View>

Let's see the difference between Android and IOS:

Android

tex-input-android

IOS

text-inpput-ios

Without the KeyboardAvoidingView component, IOS, by default, behaves precisely like Android with the windowSoftInputMode attribute set to adjustNothing. The keyboard covers the input, and the view stays unchanged.

On Android, the input is moved up and stays visible.

That is a big difference, and the reason why this implementation is the most common in projects:

   <KeyboardAvoidingView 
    behavior={Platform.OS === 'ios' ? 'padding' : undefined}
    keyboardVerticalOffset={60}
    style={styles.container}>
    {children}
   </KeyboardAvoidingView>

As shown at the beginning the effect of this code is:

Android

KeyboardAvoidingView-Android

IOS

KeyboardAvoidingView-IOS

For Android, the behavior property is set to undefined, which means do nothing; it behaves like there is no KeyboardAvoidingView component.

As I mentioned at the beginning, using this method is the best React Native can provide when using KeyboardAvoidingView.

The main problem for me is the lack of animation. The content jumps when the keyboard is being opened on Android. That is the reason I decided to create CustomKeyboardAvoidingView.

CustomKeyboardAvodingView

We don't want to use KeyboardAvoidingView anymore, meaning we need to disable the default keyboard behavior on Android. You already know by default that Android windowSoftInputMode is adjustResize. We want to change this. We want to have the same behavior as on IOS, so by default, we want to do nothing when the keyboard is opened. As you can already guess, the way to do that is to set windowSoftInputMode to adjustNothing.

For React Native:

<activity
    android:name=".YourActivity"
    android:windowSoftInputMode="adjustNothing">
    <!-- Other activity attributes -->
</activity>

For Expo in app.json file:

"expo": {
    "android": {
      "softwareKeyboardLayoutMode": "adjustNothing",
    },
}

Once done, we want to animate the screen when the user focuses on the input while the keyboard is being opened. We need to go through 2 steps.

1. Listening for keyboard opening/hiding events.

2. Animate the content while the keyboard is being opened.

For the first step, let's create the CustomKeyboardAvoidingView component and add event listeners:

import { Platform, Keyboard } from "react-native"
 
const isIOS = Platform.OS === "ios"
 
const CustomKeyboardAvoidingView = () => {
  const [keyboardOpen, setKeyboardOpen] = useState(false)
  const [keyboardHeight, setKeyboardHeight] = useState(0)
 
  useEffect(() => {
    const keyboardWillShowListener = Keyboard.addListener(
      isIOS ? "keyboardWillShow" : "keyboardDidShow",
      e => {
       setKeyboardOpen(true)
       setKeyboardHeight(e.endCoordinates.height)
      },
    )
    const keyboardWillHideListener = Keyboard.addListener(
      isIOS ? "keyboardWillHide" : "keyboardDidHide",
      e => {
        setKeyboardOpen(false)
        setKeyboardHeight(e.endCoordinates.height)
      },
    )
    return () => {
      keyboardWillShowListener.remove()
      keyboardWillHideListener.remove()
    }
  }, [])
}

As you can see, there is a difference between IOS and Android. keyboardWillShow and keyboardWillHide events are allowed only on IOS; on Android, we have to use keyboardDidShow and keyboardWillShow.

Having that knowledge, we know when the keyboard is open and the height of the keyboard. This information is necessary to animate the content.

Second step. Animate the content.

We have two options: we can use Animated API from React Native or the react-native-reanimated library. I am using Animated API because I encountered some problems with react-native-reanimated in the past using it in this component. The issues can already be solved, so feel free to use react-native-reanimated.

import { Platform, Keyboard, Animated, ViewStyle } from "react-native";
import { PropsWithChildren, useEffect, useRef, useState } from "react";
 
type CustomKeyboardAvoidingViewProps = {
  screenWithBottomTabNavigation?: boolean;
  offset?: number;
  positionBottom?: boolean;
  customStyles?: ViewStyle;
};
 
const isIOS = Platform.OS === "ios";
const INITIAL_POSITION = 0;
const BOTTOM_TAB_HEIGHT = isIOS ? 78 : 50;
 
const CustomKeyboardAvoidingView = ({
  screenWithBottomTabNavigation = false,
  offset = 0,
  positionBottom = false,
  customStyles,
  children,
}: PropsWithChildren<CustomKeyboardAvoidingViewProps>) => {
  const [keyboardOpen, setKeyboardOpen] = useState(false);
  const [keyboardHeight, setKeyboardHeight] = useState(0);
 
  const translateY = useRef(new Animated.Value(INITIAL_POSITION)).current;
 
  const getTranslateYValue = () => {
    // When the keyboard is open and the input and bottom tabNavigator is at the bottom of the screen
    if (keyboardOpen && positionBottom && screenWithBottomTabNavigation) {
      return -keyboardHeight + BOTTOM_TAB_HEIGHT;
      // Only when the keyboard is open and the input is at the bottom of the screen
    } else if (keyboardOpen && positionBottom) {
      return -keyboardHeight + offset;
      // Only when the keyboard is open, no input at the bottom of the screen and bottom tab navigator
    } else if (keyboardOpen) {
      return -offset;
    } else {
      // When keyboard is closed
      return INITIAL_POSITION;
    }
  };
 
  const animateTranslateY = () => {
    Animated.timing(translateY, {
      toValue: getTranslateYValue(),
      duration: isIOS ? 300 : 200, // on IOS the keyboard event is fired a bit faster so I put more dalay. It is up to you.
      useNativeDriver: true,
    }).start();
  };
 
  useEffect(() => {
    const keyboardWillShowListener = Keyboard.addListener(
      isIOS ? "keyboardWillShow" : "keyboardDidShow",
      (e) => {
        setKeyboardOpen(true);
        setKeyboardHeight(e.endCoordinates.height);
      }
    );
    const keyboardWillHideListener = Keyboard.addListener(
      isIOS ? "keyboardWillHide" : "keyboardDidHide",
      (e) => {
        setKeyboardOpen(false);
        setKeyboardHeight(e.endCoordinates.height);
      }
    );
    return () => {
      keyboardWillShowListener.remove();
      keyboardWillHideListener.remove();
    };
  }, []);
 
  useEffect(() => {
    animateTranslateY();
  }, [keyboardHeight, keyboardOpen]);
 
  return (
    <Animated.View style={[customStyles, { transform: [{ translateY }] }]}>
      {children}
    </Animated.View>
  );
};
 
export { CustomKeyboardAvoidingView };

The preivew of this code:

Android

CustomKeyboardAvoidingView-Android

IOS

CustomKeyboardAvoidingView-IOS

It looks nice, but I would like to improve it. Now, we move the whole content up, and the content goes out of the screen.

It would be a much better user experience when the content fits the screen while the keyboard is open. We accomplish that with animating paddingTop.

import { Platform, Keyboard, Animated, ViewStyle } from "react-native";
import { PropsWithChildren, useEffect, useRef, useState } from "react";
 
type CustomKeyboardAvoidingViewProps = {
  screenWithBottomTabNavigation?: boolean;
  offset?: number;
  positionBottom?: boolean;
  animatedPaddingTopValue?: mumber
  customStyles?: ViewStyle;
};
 
const isIOS = Platform.OS === "ios";
const INITIAL_POSITION = 0;
const INITIAL_PADDING_TOP = 0;
const BOTTOM_TAB_HEIGHT = isIOS ? 78 : 50;
 
const CustomKeyboardAvoidingView = ({
  screenWithBottomTabNavigation = false,
  offset = 0,
  positionBottom = false,
  animatedPaddingTopValue = 0,
  customStyles,
  children,
}: PropsWithChildren<CustomKeyboardAvoidingViewProps>) => {
  const [keyboardOpen, setKeyboardOpen] = useState(false);
  const [keyboardHeight, setKeyboardHeight] = useState(0);
 
  const translateY = useRef(new Animated.Value(INITIAL_POSITION)).current;
  const paddingTop = useRef(new Animated.Value(INITIAL_PADDING_TOP)).current;
 
  const getTranslateYValue = () => {
    // When the keyboard is open and the input and bottom tabNavigator is at the bottom of the screen
    if (keyboardOpen && positionBottom && screenWithBottomTabNavigation) {
      return -keyboardHeight + BOTTOM_TAB_HEIGHT;
      // Only when the keyboard is open and the input is at the bottom of the screen
    } else if (keyboardOpen && positionBottom) {
      return -keyboardHeight + offset;
      // Only when the keyboard is open, no input at the bottom of the screen and bottom tab navigator
    } else if (keyboardOpen) {
      return -offset;
    } else {
      // When keyboard is closed
      return INITIAL_POSITION;
    }
  };
 
  const animateValues = () => {
    Animated.parallel([
      Animated.timing(translateY, {
        toValue: getTranslateYValue(),
        duration: isIOS ? 300 : 200,
        useNativeDriver: false,
      }),
      Animated.timing(paddingTop, {
        toValue: keyboardOpen ? animatedPaddingTopValue : 0,
        duration: isIOS ? 300 : 200,
        useNativeDriver: false,
      }),
    ]).start();
  };
 
  useEffect(() => {
    const keyboardWillShowListener = Keyboard.addListener(
      isIOS ? "keyboardWillShow" : "keyboardDidShow",
      (e) => {
        setKeyboardOpen(true);
        setKeyboardHeight(e.endCoordinates.height);
      }
    );
    const keyboardWillHideListener = Keyboard.addListener(
      isIOS ? "keyboardWillHide" : "keyboardDidHide",
      (e) => {
        setKeyboardOpen(false);
        setKeyboardHeight(e.endCoordinates.height);
      }
    );
    return () => {
      keyboardWillShowListener.remove();
      keyboardWillHideListener.remove();
    };
  }, []);
 
  useEffect(() => {
    animateValues();
  }, [keyboardHeight, keyboardOpen]);
 
  return (
    <Animated.View
      style={[customStyles, { transform: [{ translateY }], paddingTop }]}
    >
      {children}
    </Animated.View>
  );
};
 
export { CustomKeyboardAvoidingView };

Final effects:

Android

CustomKeyboardAvoidingView-Android

IOS

CustomKeyboardAvoidingView-IOS

The whole app code, which you can see in the GIFs, is available here.

Github repository - CustomKeyboardAvoidingView -->

Previews from the app:

Screen without bottom tab:

Android

CustomKeyboardAvoidingView-Android

IOS

CustomKeyboardAvoidingView-IOS

Login screen with inputs in the middle:

Android

CustomKeyboardAvoidingView-Android

IOS

CustomKeyboardAvoidingView-IOS

Screen with button at the bottom:

Android

CustomKeyboardAvoidingView-Android

IOS

CustomKeyboardAvoidingView-IOS

If you find this article helpful, please share it on your social media. Also let's connect on Linkedin.

Thanks!

Comments