import * as React from 'react'; import { Animated, Keyboard, StyleSheet } from 'react-native'; import ViewPager, { PageScrollStateChangedNativeEvent, } from 'react-native-pager-view'; import type { EventEmitterProps, Listener, NavigationState, PagerProps, Route, } from './types'; import { useAnimatedValue } from './useAnimatedValue'; const AnimatedViewPager = Animated.createAnimatedComponent(ViewPager); type Props = PagerProps & { onIndexChange: (index: number) => void; navigationState: NavigationState; children: ( props: EventEmitterProps & { // Animated value which represents the state of current index // It can include fractional digits as it represents the intermediate value position: Animated.AnimatedInterpolation; // Function to actually render the content of the pager // The parent component takes care of rendering render: (children: React.ReactNode) => React.ReactNode; // Callback to call when switching the tab // The tab switch animation is performed even if the index in state is unchanged jumpTo: (key: string) => void; } ) => React.ReactElement; }; export function PagerViewAdapter({ keyboardDismissMode = 'auto', swipeEnabled = true, navigationState, onIndexChange, onSwipeStart, onSwipeEnd, children, style, animationEnabled, ...rest }: Props) { const { index } = navigationState; const listenersRef = React.useRef([]); const pagerRef = React.useRef(); const indexRef = React.useRef(index); const navigationStateRef = React.useRef(navigationState); const position = useAnimatedValue(index); const offset = useAnimatedValue(0); React.useEffect(() => { navigationStateRef.current = navigationState; }); const jumpTo = React.useCallback( (key: string) => { const index = navigationStateRef.current.routes.findIndex( (route: { key: string }) => route.key === key ); if (animationEnabled) { pagerRef.current?.setPage(index); } else { pagerRef.current?.setPageWithoutAnimation(index); position.setValue(index); } }, [animationEnabled, position] ); React.useEffect(() => { if (keyboardDismissMode === 'auto') { Keyboard.dismiss(); } if (indexRef.current !== index) { if (animationEnabled) { pagerRef.current?.setPage(index); } else { pagerRef.current?.setPageWithoutAnimation(index); position.setValue(index); } } }, [keyboardDismissMode, index, animationEnabled, position]); const onPageScrollStateChanged = ( state: PageScrollStateChangedNativeEvent ) => { const { pageScrollState } = state.nativeEvent; switch (pageScrollState) { case 'idle': onSwipeEnd?.(); return; case 'dragging': { const subscription = offset.addListener(({ value }) => { const next = index + (value > 0 ? Math.ceil(value) : Math.floor(value)); if (next !== index) { listenersRef.current.forEach((listener) => listener(next)); } offset.removeListener(subscription); }); onSwipeStart?.(); return; } } }; const addEnterListener = React.useCallback((listener: Listener) => { listenersRef.current.push(listener); return () => { const index = listenersRef.current.indexOf(listener); if (index > -1) { listenersRef.current.splice(index, 1); } }; }, []); const memoizedPosition = React.useMemo( () => Animated.add(position, offset), [offset, position] ); return children({ position: memoizedPosition, addEnterListener, jumpTo, render: (children) => ( { const index = e.nativeEvent.position; indexRef.current = index; onIndexChange(index); }} onPageScrollStateChanged={onPageScrollStateChanged} scrollEnabled={swipeEnabled} > {children} ), }); } const styles = StyleSheet.create({ container: { flex: 1, }, });