mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 08:33:39 +02:00
Projects included: - maerchenzauber (NestJS backend + Expo mobile + SvelteKit web + Astro landing) - manacore (Expo mobile + SvelteKit web + Astro landing) - manadeck (NestJS backend + Expo mobile + SvelteKit web) - memoro (Expo mobile + SvelteKit web + Astro landing) This commit preserves the current state before monorepo restructuring. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
373 lines
13 KiB
Markdown
373 lines
13 KiB
Markdown
Keyboard handling
|
|
|
|
A guide for handling common keyboard interactions on an Android or iOS device.
|
|
|
|
Keyboard handling is crucial for creating an excellent user experience in your Expo app. React Native provides Keyboard and KeyboardAvoidingView, which are commonly used to handle keyboard events. For more complex or custom keyboard interactions, you can consider using react-native-keyboard-controller, which is a library that offers advanced keyboard handling capabilities.
|
|
|
|
This guide covers common keyboard interactions and how to manage them effectively.
|
|
|
|
Keyboard Handling tutorial for React Native apps
|
|
Keyboard Handling tutorial for React Native apps
|
|
In this keyboard handling tutorial for React Native apps, you'll learn how to solve the problem of the keyboard covering your input when you try to type on your app.
|
|
|
|
Keyboard handling basics
|
|
The following sections explain how to handle keyboard interactions with common APIs.
|
|
|
|
Keyboard avoiding view
|
|
The KeyboardAvoidingView is a component that automatically adjusts a keyboard's height, position, or bottom padding based on the keyboard height to remain visible while it is displayed.
|
|
|
|
Android and iOS interact with the behavior property differently. On iOS, padding is usually what works best, and for Android, just having the KeyboardAvoidingView prevents covering the input. This is why the following example uses undefined for Android. Playing around with the behavior is a good practice since a different option could work best for your app.
|
|
|
|
HomeScreen.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { KeyboardAvoidingView, TextInput } from 'react-native';
|
|
|
|
export default function HomeScreen() {
|
|
return (
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
|
<TextInput placeholder="Type here..." />
|
|
</KeyboardAvoidingView>;
|
|
);
|
|
}
|
|
In the above example, the height of the KeyboardAvoidingView automatically adjusts based on the device's keyboard height, which ensures that the input is always visible.
|
|
|
|
When using a Bottom Tab navigator on Android, you might notice that focusing on an input field causes the bottom tabs to be pushed above the keyboard. To address this issue, add the softwareKeyboardLayoutMode property to your Android configuration in app confg and set it to pan.
|
|
|
|
app.json
|
|
|
|
Copy
|
|
|
|
|
|
"expo" {
|
|
"android": {
|
|
"softwareKeyboardLayoutMode": "pan"
|
|
}
|
|
}
|
|
After adding this property, restart the development server and reload your app to apply the changes.
|
|
|
|
It's also possible to hide the bottom tab when the keyboard opens using tabBarHideOnKeyboard. It is an option with the Bottom Tab Navigator. If set to true, it will hide the bar when the keyboard opens.
|
|
|
|
app/_layout.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { Tabs } from 'expo-router';
|
|
|
|
export default function TabLayout() {
|
|
return (
|
|
<Tabs
|
|
screenOptions={{
|
|
tabBarHideOnKeyboard: true,
|
|
}}>
|
|
<Tabs.Screen name="index" />
|
|
</Tabs>
|
|
);
|
|
}
|
|
Keyboard events
|
|
The Keyboard module from React Native allows you to listen for native events, react to them, and make changes to the keyboard, such as dismissing it.
|
|
|
|
To listen for keyboard events, use the Keyboard.addListener method. This method accepts an event name and a callback function as arguments. When the keyboard is shown or hidden, the callback function is called with the event data.
|
|
|
|
The following example illustrates a use case for adding a keyboard listener. The state variable isKeyboardVisible is toggled each time the keyboard shows or hides. Based on this variable, a button allows the user to dismiss the keyboard only if the keyboard is active. Also, notice that the button uses the Keyboard.dismiss method.
|
|
|
|
HomeScreen.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Keyboard, View, Button, TextInput } from 'react-native';
|
|
|
|
export default function HomeScreen() {
|
|
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const showSubscription = Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
|
|
const hideSubscription = Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
|
|
|
|
return () => {
|
|
showSubscription.remove();
|
|
};
|
|
}, []);
|
|
|
|
const handleKeyboardShow = event => {
|
|
setIsKeyboardVisible(true);
|
|
};
|
|
|
|
const handleKeyboardHide = event => {
|
|
setIsKeyboardVisible(false);
|
|
};
|
|
|
|
return (
|
|
<View>
|
|
{isKeyboardVisible && <Button title="Dismiss keyboard" onPress={Keyboard.dismiss} />}
|
|
<TextInput placeholder="Type here..." />
|
|
</View>
|
|
);
|
|
}
|
|
Advanced keyboard handling with Keyboard Controller
|
|
For complex forms with multiple input fields, the recommended solution is the KeyboardAwareScrollView component. This component provides the best keyboard handling experience, especially when dealing with multiple text inputs.
|
|
|
|
### KeyboardAwareScrollView Implementation
|
|
|
|
First, install the package:
|
|
```bash
|
|
npm install react-native-keyboard-aware-scroll-view --save
|
|
```
|
|
|
|
Here's a complete example with best practices:
|
|
|
|
```tsx
|
|
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
|
|
|
|
export default function Form() {
|
|
return (
|
|
<KeyboardAwareScrollView
|
|
enableOnAndroid={true} // Enable functionality for Android
|
|
enableAutomaticScroll={true} // Automatically scroll to focused input
|
|
keyboardShouldPersistTaps="handled" // Allow interaction with inputs while keyboard is visible
|
|
extraScrollHeight={Platform.OS === 'ios' ? 120 : 40} // Extra space above keyboard
|
|
keyboardOpeningTime={0} // No delay when keyboard opens
|
|
contentContainerStyle={styles.contentContainer}
|
|
>
|
|
<View style={styles.container}>
|
|
<TextInput />
|
|
{/* More form fields */}
|
|
</View>
|
|
</KeyboardAwareScrollView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
padding: 16,
|
|
},
|
|
contentContainer: {
|
|
flexGrow: 1,
|
|
paddingBottom: 120, // Additional space at bottom for better scrolling
|
|
}
|
|
});
|
|
```
|
|
|
|
Key configuration options:
|
|
1. `enableOnAndroid`: Ensures consistent behavior across platforms
|
|
2. `extraScrollHeight`: Provides extra space above keyboard (iOS typically needs more)
|
|
3. `keyboardShouldPersistTaps="handled"`: Allows better interaction with inputs
|
|
4. `contentContainerStyle`: Use flexGrow for proper content scaling
|
|
5. `keyboardOpeningTime={0}`: Immediate scrolling response
|
|
|
|
Real-world example from the Storyteller app:
|
|
```tsx
|
|
<KeyboardAwareScrollView
|
|
contentContainerStyle={styles.contentContainer}
|
|
enableOnAndroid={true}
|
|
enableAutomaticScroll={true}
|
|
keyboardShouldPersistTaps="handled"
|
|
extraScrollHeight={Platform.OS === 'ios' ? 120 : 40}
|
|
keyboardOpeningTime={0}
|
|
>
|
|
<View style={styles.container}>
|
|
<TextField
|
|
placeholder="Input field"
|
|
// ... other props
|
|
/>
|
|
</View>
|
|
</KeyboardAwareScrollView>
|
|
```
|
|
|
|
This implementation ensures that:
|
|
- Input fields automatically scroll into view when focused
|
|
- The keyboard doesn't cover any input fields
|
|
- Scrolling behavior is smooth and consistent across platforms
|
|
- Users can interact with the form while the keyboard is visible
|
|
|
|
For more complex keyboard interactions or custom animations, you might want to consider using the react-native-keyboard-controller library. It offers additional functionality beyond the built-in React Native keyboard APIs.
|
|
|
|
Prerequisites
|
|
The following steps are described using a development build since the Keyboard Controller library is not included in Expo Go. See Create a development build for more information.
|
|
|
|
Keyboard Controller also requires react-native-reanimated to work correctly. To install it, follow these installation instructions.
|
|
|
|
Install
|
|
Start by installing the Keyboard Controller library in your Expo project:
|
|
|
|
Terminal
|
|
|
|
Copy
|
|
|
|
npx expo install react-native-keyboard-controller
|
|
Set up provider
|
|
To finalize the setup, add the KeyboardProvider to your app.
|
|
|
|
app/_layout.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { Stack } from 'expo-router';
|
|
import { KeyboardProvider } from 'react-native-keyboard-controller';
|
|
|
|
export default function RootLayout() {
|
|
return (
|
|
<KeyboardProvider>
|
|
<Stack>
|
|
<Stack.Screen name="home" />
|
|
<Stack.Screen name="chat" />
|
|
</Stack>
|
|
</KeyboardProvider>
|
|
);
|
|
}
|
|
Handling multiple inputs
|
|
The KeyboardAvoidingView component is excellent for prototyping but requires platform-specific configuration and is not very customizable. To achieve the same functionality, you can use KeyboardAwareScrollView, which automatically scrolls to focused TextInput and provides a native-like performance, we recommend using KeyboardAwareScrollView for simple screens with not many elements.
|
|
|
|
For screens with multiple inputs, the Keyboard Controller provides KeyboardAwareScrollView and KeyboardToolbar components. These components handle input navigation and prevent the keyboard from covering the screen without custom configuration:
|
|
|
|
FormScreen.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { TextInput, View, StyleSheet } from 'react-native';
|
|
import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';
|
|
|
|
export default function FormScreen() {
|
|
return (
|
|
<>
|
|
<KeyboardAwareScrollView bottomOffset={62} contentContainerStyle={styles.container}>
|
|
<View>
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
</View>
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
<View>
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
</View>
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
</KeyboardAwareScrollView>
|
|
<KeyboardToolbar />
|
|
</>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
gap: 16,
|
|
padding: 16,
|
|
},
|
|
listStyle: {
|
|
padding: 16,
|
|
gap: 16,
|
|
},
|
|
textInput: {
|
|
width: 'auto',
|
|
flexGrow: 1,
|
|
flexShrink: 1,
|
|
height: 45,
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
borderColor: '#d8d8d8',
|
|
backgroundColor: '#fff',
|
|
padding: 8,
|
|
marginBottom: 8,
|
|
},
|
|
});
|
|
The above example wraps the inputs with KeyboardAwareScrollView to prevent the keyboard from covering them. The KeyboardToolbar component displays navigation controls and a dismiss button. While it works without configuration, you can customize the toolbar content if needed.
|
|
|
|
Animating views in sync with keyboard height
|
|
For a more advanced and customizable approach, you can use useKeyboardHandler. It provides access to keyboard lifecycle events. It allows us to determine when the keyboard starts animating and its position in every frame of the animation.
|
|
|
|
Using the useKeyboardHandler hook, you can create a custom hook to access the height of the keyboard at each frame. It uses useSharedValue from reanimated to return the height, as shown below.
|
|
|
|
ChatScreen.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
|
|
const useGradualAnimation = () => {
|
|
const height = useSharedValue(0);
|
|
|
|
useKeyboardHandler(
|
|
{
|
|
onMove: event => {
|
|
'worklet';
|
|
height.value = Math.max(event.height, 0);
|
|
},
|
|
},
|
|
[]
|
|
);
|
|
return { height };
|
|
};
|
|
You can use the useGradualAnimation hook to animate a view and give it a smooth animation when the keyboard is active or dismissed, for example, in a chat screen component (shown in the example below). This component gets the keyboard height from the hook. It then creates an animated style called fakeView using the useAnimatedStyle hook from reanimated. This style only contains one property: height, which is set to the keyboard's height.
|
|
|
|
The fakeView animated style is used in an animated view after the TextInput. This view's height will animate based on the keyboard's height at each frame, which effectively pushes the content above the keyboard with a smooth animation. It also decreases its height to zero when the keyboard is dismissed.
|
|
|
|
ChatScreen.tsx
|
|
|
|
Copy
|
|
|
|
|
|
import { StyleSheet, Platform, FlatList, View, StatusBar, TextInput } from 'react-native';
|
|
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
|
|
import { useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
|
|
import MessageItem from '@/components/MessageItem';
|
|
import { messages } from '@/messages';
|
|
|
|
const useGradualAnimation = () => {
|
|
// Code remains same from previous example
|
|
};
|
|
|
|
export default function ChatScreen() {
|
|
const { height } = useGradualAnimation();
|
|
|
|
const fakeView = useAnimatedStyle(() => {
|
|
return {
|
|
height: Math.abs(height.value),
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<FlatList
|
|
data={messages}
|
|
renderItem={({ item }) => <MessageItem message={item} />}
|
|
keyExtractor={item => item.createdAt.toString()}
|
|
contentContainerStyle={styles.listStyle}
|
|
/>
|
|
<TextInput placeholder="Type a message..." style={styles.textInput} />
|
|
<Animated.View style={fakeView} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
|
|
},
|
|
listStyle: {
|
|
padding: 16,
|
|
gap: 16,
|
|
},
|
|
textInput: {
|
|
width: '95%',
|
|
height: 45,
|
|
borderWidth: 1,
|
|
borderRadius: 8,
|
|
borderColor: '#d8d8d8',
|
|
backgroundColor: '#fff',
|
|
padding: 8,
|
|
alignSelf: 'center',
|
|
marginBottom: 8,
|
|
},
|
|
});
|