import React, { useEffect, useRef } from 'react'; import { Animated, Keyboard, Platform, ScrollView, type ScrollViewProps, } from 'react-native'; export interface KeyboardAvoidingViewProps extends ScrollViewProps { /** * Extra height to add above the keyboard. * @default 24 */ extraScrollHeight?: number; /** * Whether keyboard avoiding behavior is enabled. * @default true */ enabled?: boolean; /** * Additional offset from the keyboard top. * @default 0 */ keyboardVerticalOffset?: number; } /** * A custom KeyboardAvoidingView built on top of ScrollView. * * - iOS: Uses `automaticallyAdjustKeyboardInsets` for native keyboard handling * - Android: Uses animated bottom padding to avoid the keyboard * * @example * ```tsx * * * * * * ``` */ export function KeyboardAvoidingView({ children, extraScrollHeight = 24, enabled = true, keyboardVerticalOffset = 0, contentContainerStyle, ...scrollViewProps }: KeyboardAvoidingViewProps) { const scrollViewRef = useRef(null); const animatedPadding = useRef(new Animated.Value(0)).current; // Android: manual keyboard handling with animated padding useEffect(() => { if (!enabled || Platform.OS === 'ios') return; const showListener = Keyboard.addListener('keyboardDidShow', (event) => { Animated.timing(animatedPadding, { toValue: event.endCoordinates.height + keyboardVerticalOffset + extraScrollHeight, duration: 250, useNativeDriver: false, }).start(); }); const hideListener = Keyboard.addListener('keyboardDidHide', () => { Animated.timing(animatedPadding, { toValue: 0, duration: 250, useNativeDriver: false, }).start(); }); return () => { showListener.remove(); hideListener.remove(); }; }, [enabled, animatedPadding, keyboardVerticalOffset, extraScrollHeight]); return ( {children} {Platform.OS !== 'ios' && ( )} ); } /** * A simpler variant that just adds keyboard-aware bottom padding * without ScrollView wrapping — useful when you already have a ScrollView. * * @example * ```tsx * * * * * ``` */ export function KeyboardSpacer({ extraHeight = 0, enabled = true, }: { extraHeight?: number; enabled?: boolean; }) { const animatedHeight = useRef(new Animated.Value(0)).current; useEffect(() => { if (!enabled) return; const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow'; const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide'; const showListener = Keyboard.addListener(showEvent, (event) => { Animated.timing(animatedHeight, { toValue: event.endCoordinates.height + extraHeight, duration: Platform.OS === 'ios' ? event.duration ?? 250 : 250, useNativeDriver: false, }).start(); }); const hideListener = Keyboard.addListener(hideEvent, (event) => { Animated.timing(animatedHeight, { toValue: 0, duration: Platform.OS === 'ios' ? event.duration ?? 250 : 250, useNativeDriver: false, }).start(); }); return () => { showListener.remove(); hideListener.remove(); }; }, [enabled, animatedHeight, extraHeight]); return ; }