Skip to content

Instantly share code, notes, and snippets.

@alexandrius
Last active July 22, 2024 17:49
Show Gist options
  • Save alexandrius/f264251515f2a9b453fa9aeb253574b4 to your computer and use it in GitHub Desktop.
Save alexandrius/f264251515f2a9b453fa9aeb253574b4 to your computer and use it in GitHub Desktop.
import { useMemo, useState } from 'react';
import { type ViewProps, StyleSheet } from 'react-native';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
interpolate,
useAnimatedStyle,
useSharedValue,
withTiming,
Easing,
type SharedValue,
Extrapolation,
runOnJS,
} from 'react-native-reanimated';
import { getTopInset } from 'rn-iphone-helper';
import { PageProps } from '../pages/PageProps';
import { iOS } from '@/constants/bits';
import { heights, widths } from '@/constants/dimens';
import useBackButton from '@/hooks/useBackButton';
const { screen: screenWidth } = widths;
type NavigatorProps = {
pages: React.FC<PageProps>[];
dismissSheet: PageProps['dismissSheet'];
};
type NavigatorPageProps = {
index: number;
children: ViewProps['children'];
animation: SharedValue<number>;
inputRange: number[];
};
const animationConfig = {
duration: 150,
easing: Easing.inOut(Easing.quad),
};
const GESTURE_DETECTOR_TOP = 60;
const styles = StyleSheet.create({
gestureDetector: {
position: 'absolute',
height: heights.screen - getTopInset() - GESTURE_DETECTOR_TOP,
top: GESTURE_DETECTOR_TOP,
width: 40,
},
});
const NavigatorPage = ({ index, children, animation, inputRange }: NavigatorPageProps) => {
const outputRange = useMemo(() => {
return inputRange.map((_, i) => {
if (i === index) return 0;
else if (i > index) return -screenWidth * 0.3;
return screenWidth;
});
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: interpolate(animation.value, inputRange, outputRange, Extrapolation.CLAMP),
},
],
};
});
return (
<Animated.View className='absolute w-full h-full bg-light dark:bg-dark' style={animatedStyle}>
{children}
</Animated.View>
);
};
export default function Navigator({ pages, dismissSheet }: NavigatorProps) {
const animation = useSharedValue(0);
const [currentIndex, setCurrentIndex] = useState(0);
const setCurrentIndexUI = (index: number) => {
'worklet';
runOnJS(setCurrentIndex)(index);
};
const goBack = () => {
const nextIndex = currentIndex - 1;
animation.value = withTiming(nextIndex, animationConfig, () => {
setCurrentIndexUI(nextIndex);
});
};
const goNext = () => {
const nextIndex = currentIndex + 1;
animation.value = withTiming(nextIndex, animationConfig, () => {
setCurrentIndexUI(nextIndex);
});
};
useBackButton(() => {
goBack();
}, animation.value > 0);
const inputRange = useMemo(() => {
return Array.from({ length: pages.length }, (_, index) => {
return index;
});
}, []);
const panGesture = Gesture.Pan()
.enabled(currentIndex > 0)
.activeOffsetX(5)
.onUpdate(({ translationX }) => {
if (translationX > 0) {
animation.value = currentIndex - translationX / screenWidth;
}
})
.onEnd(({ translationX, velocityX }) => {
if (translationX > screenWidth / 5 || velocityX > 500) {
runOnJS(goBack)();
} else {
animation.value = withTiming(currentIndex, animationConfig);
}
});
return (
<>
{pages.map((Component, index) => {
return (
<NavigatorPage key={index.toString()} {...{ index, animation, inputRange }}>
<Component dismissSheet={dismissSheet} nextPage={goNext} prevPage={goBack} />
</NavigatorPage>
);
})}
{iOS && (
<GestureDetector gesture={panGesture}>
<Animated.View style={styles.gestureDetector} />
</GestureDetector>
)}
</>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment