DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on

Building a Smooth, Keyboard-Aware Component in React Native

Image description

Creating a well-optimized sign-in screen in React Native often involves handling keyboard interactions to avoid input fields being hidden by the keyboard. In this guide, we’ll walk through building a keyboard-aware sign-in screen with animated adjustments, leveraging a custom hook to manage keyboard offset height. We’ll also add a header image and organize the screen for an aesthetically pleasing and functional layout.

Features of this Implementation:

  1. Keyboard Awareness: The screen adjusts its position based on the keyboard height.
  2. Smooth Animation: Animated transitions when the keyboard appears or disappears.
  3. Reusable Custom Hook: A useKeyboardOffsetHeight hook to manage keyboard height dynamically.

1. Custom Hook: useKeyboardOffsetHeight

The custom hook useKeyboardOffsetHeight listens for keyboard show/hide events and returns the keyboard height, which is crucial for animating the layout adjustments. This hook also ensures the functionality works across both iOS and Android.

import { useEffect, useState } from 'react';
import { Keyboard } from 'react-native';

export default function useKeyboardOffsetHeight() {
  const [keyboardOffsetHeight, setKeyboardOffsetHeight] = useState(0);

  useEffect(() => {
    const showListener = Keyboard.addListener('keyboardWillShow', (e) => {
      setKeyboardOffsetHeight(e.endCoordinates.height);
    });
    const hideListener = Keyboard.addListener('keyboardWillHide', () => {
      setKeyboardOffsetHeight(0);
    });

    const androidShowListener = Keyboard.addListener('keyboardDidShow', (e) => {
      setKeyboardOffsetHeight(e.endCoordinates.height);
    });
    const androidHideListener = Keyboard.addListener('keyboardDidHide', () => {
      setKeyboardOffsetHeight(0);
    });

    return () => {
      showListener.remove();
      hideListener.remove();
      androidShowListener.remove();
      androidHideListener.remove();
    };
  }, []);

  return keyboardOffsetHeight;
}
Enter fullscreen mode Exit fullscreen mode

2. Animated Scale Button:

ScalePress.js

import { View, Text, Animated, TouchableOpacity } from 'react-native';
import React from 'react';

const ScalePress = ({ onLongPress, onPress, children, style }) => {
  const scaleValue = new Animated.Value(1);

  const onPressIn = () => {
    Animated.spring(scaleValue, {
      toValue: 0.96,
      useNativeDriver: true,
    }).start();
  };

  const onPressOut = () => {
    Animated.timing(scaleValue, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  return (
    <TouchableOpacity
      onPress={onPress}
      onPressOut={onPressOut}
      onLongPress={onLongPress}
      onPressIn={onPressIn}
      activeOpacity={1}
      style={style}
    >
      <Animated.View style={{ transform: [{ scale: scaleValue }], width: '100%' }}>
        {children}
      </Animated.View>
    </TouchableOpacity>
  );
};

export default ScalePress;
Enter fullscreen mode Exit fullscreen mode

3. Main Component: App

The main component uses the custom useKeyboardOffsetHeight hook and the Animated API to manage smooth transitions for the sign-in form. The form includes email and password fields, a sign-in button, and a header image.

import React, { useEffect, useRef, useState } from 'react';
import { Animated, Image, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native';
import useKeyboardOffsetHeight from './useKeyboardOffsetHeight';

const App = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const keyboardOffsetHeight = useKeyboardOffsetHeight();
  const animatedValue = useRef(new Animated.Value(0)).current;

  const handleSignIn = () => {
    // Handle sign-in logic here
    console.log('Email:', email);
    console.log('Password:', password);
  };

  // Animate view based on keyboard height
  useEffect(() => {
    Animated.timing(animatedValue, {
      toValue: keyboardOffsetHeight ? -keyboardOffsetHeight * 0.5 : 0, // adjust "0.5" as per requirement to adjust scroll position
      duration: 500,
      useNativeDriver: true,
    }).start();
  }, [keyboardOffsetHeight]);

  return (
    <View style={styles.container}>
      <View style={{ flex: 1 }}>
        <Image
          source={{
            uri: 'https://cdn.shopaccino.com/igmguru/articles/Become-React-Native-Developer.png?v=496',
          }}
          style={styles.image}
          resizeMode="cover"
        />
      </View>
      <Animated.ScrollView
        bounces={false}
        keyboardShouldPersistTaps="handled"
        keyboardDismissMode="on-drag"
        style={{ transform: [{ translateY: animatedValue }] }}
        contentContainerStyle={styles.box}
      >
        <Text style={styles.title}>Sign In</Text>
        <TextInput
          style={styles.input}
          placeholder="Email"
          value={email}
          onChangeText={setEmail}
          keyboardType="email-address"
          autoCapitalize="none"
        />
        <TextInput
          style={styles.input}
          placeholder="Password"
          value={password}
          onChangeText={setPassword}
          secureTextEntry
        />
      </Animated.ScrollView>

     <ScalePress style={styles.signInButton} onPress={handleSignIn}>
        <Text style={styles.buttonText}>Sign In</Text>
     </ScalePress>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f9f9f9',
  },
  image: {
    flex: 1,
    borderRadius: 10,
  },
  box: {
    flex: 1,
    width: '100%',
    backgroundColor: 'lightblue',
    padding: 20,
    borderRadius: 10,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
  },
  input: {
    height: 50,
    borderColor: '#ddd',
    borderWidth: 1,
    borderRadius: 8,
    paddingHorizontal: 10,
    marginBottom: 15,
    fontSize: 16,
    backgroundColor: '#f9f9f9',
  },
  signInButton: {
    width: '100%',
    marginTop: 20,
    backgroundColor: '#4a90e2',
    borderRadius: 8,
    paddingVertical: 15,
    alignItems: 'center',
    marginBottom: 40,
  },
  buttonText: {
    color: '#fff',
    fontSize: 18,
    fontWeight: 'bold',
  },
});

export default App;
Enter fullscreen mode Exit fullscreen mode

Summary

This keyboard-aware sign-in screen provides a smooth, user-friendly experience by:

  • Using a custom hook to manage the keyboard offset height dynamically.
  • Applying animations to keep the form visible when the keyboard is active.
  • Structuring a visually appealing layout with an image header, well-styled input fields, and a prominent sign-in button.

By using this approach, you create a polished and functional text input UI, especially on mobile devices where screen space and user interactions with the keyboard are critical considerations. This setup can be expanded with more form fields or features, providing a great foundation for any React Native authentication flow.

Top comments (0)