import React, { useCallback, useEffect, useMemo } from 'react'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { FadeIn, interpolate, runOnJS, useAnimatedStyle, useDerivedValue, useSharedValue, withDecay, withSpring, withTiming, } from 'react-native-reanimated'; import { snapPoint } from 'react-native-redash'; import { ANGLE_THRESHOLD, CLAMPED_DISTANCE, SNAP_EACH, SPRING_CONFIG, } from './constants'; import { findSegmentIndex } from './helper'; import { type StackEachProps, SWIPE_POSITION, SWIPE_POSITION_KEY_INDEX, } from './types'; import Layouts from './layouts'; import { Dimensions } from 'react-native'; const { width, height } = Dimensions.get('window'); const StackEach = (props: StackEachProps) => { const { layouts, onSwipe, data, zIndex, renderItem, itemIndex, endOpacity, selection, isActivated, } = props; const indexTransition = useSharedValue(itemIndex); const [transitionX, transitionY] = [useSharedValue(0), useSharedValue(0)]; const snapSelector = (layout?: React.ReactNode, snapValue?: number) => { return layout ? (snapValue ?? 0) : 0; }; const snapPointsX = useMemo(() => { return [ snapSelector(layouts.left, -SNAP_EACH), 0, snapSelector(layouts.right, SNAP_EACH), ]; }, [layouts.left, layouts.right]); const snapPointsY = useMemo(() => { return [ snapSelector(layouts.top, -height), 0, snapSelector(layouts.bottom, height), ]; }, [layouts.bottom, layouts.top]); const handleChange = useCallback( (segment: SWIPE_POSITION) => { onSwipe?.(segment, data); }, [data, onSwipe] ); const gesture = useMemo(() => { return Gesture.Pan() .onEnd((event) => { const snapX = snapPoint( event.translationX, event.velocityX, snapPointsX ); const snapY = snapPoint( event.translationY, event.velocityY, snapPointsY ); const segment = findSegmentIndex( event.translationX, event.translationY ); const snapSegment = [ SWIPE_POSITION.RIGHT, SWIPE_POSITION.LEFT, ].includes(segment) ? snapX : snapY; switch (segment) { case SWIPE_POSITION.RIGHT: case SWIPE_POSITION.LEFT: transitionX.value = withSpring(snapX, SPRING_CONFIG); transitionY.value = snapY === 0 ? withSpring(0, SPRING_CONFIG) : withDecay({ velocity: event.velocityY }); break; case SWIPE_POSITION.BOTTOM: case SWIPE_POSITION.TOP: transitionX.value = snapX === 0 ? withSpring(0, SPRING_CONFIG) : withDecay({ velocity: event.velocityX }); transitionY.value = withSpring(snapY, SPRING_CONFIG); break; default: break; } if ( layouts[SWIPE_POSITION_KEY_INDEX[segment] as keyof typeof layouts] ) { snapSegment !== 0 && runOnJS(handleChange)(segment); } else { transitionX.value = withSpring(0, SPRING_CONFIG); transitionY.value = withSpring(0, SPRING_CONFIG); } }) .onChange((event) => { transitionX.value = event.translationX; transitionY.value = event.translationY; }) .enabled(itemIndex === 0); }, [ handleChange, itemIndex, layouts, snapPointsX, snapPointsY, transitionX, transitionY, ]); const segment = useDerivedValue(() => { 'worklet'; return findSegmentIndex(transitionX.value, transitionY.value); }); const distance = useDerivedValue(() => { 'worklet'; const calculated = Math.sqrt( transitionX.value ** 2 + transitionY.value ** 2 ); return interpolate( calculated, [0, ...CLAMPED_DISTANCE, SNAP_EACH], [0, 0, endOpacity!, endOpacity!] ); }); const style = useAnimatedStyle(() => { const rotate = interpolate( transitionX.value, [-width, width], [-ANGLE_THRESHOLD, ANGLE_THRESHOLD] ); return { transform: [ { translateX: transitionX.value }, { translateY: transitionY.value }, { rotate: `${rotate}deg` }, ], }; }); useEffect(() => { if (isActivated) { switch (selection?.position) { case SWIPE_POSITION.RIGHT: transitionX.value = snapPointsX[2]!; break; case SWIPE_POSITION.LEFT: transitionX.value = snapPointsX[0]!; break; case SWIPE_POSITION.TOP: transitionY.value = snapPointsY[0]!; break; case SWIPE_POSITION.BOTTOM: transitionY.value = snapPointsY[2]!; break; default: break; } } transitionX.value = withSpring(0, SPRING_CONFIG); transitionY.value = withSpring(0, SPRING_CONFIG); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { indexTransition.value = withTiming(itemIndex); // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemIndex]); useEffect(() => { indexTransition.value = withTiming(itemIndex); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const indexStyle = useAnimatedStyle(() => { const scale = interpolate(indexTransition.value, [0, 1], [1, 0.97]); const translateY = interpolate(indexTransition.value, [0, 1], [0, -20]); return { transform: [{ scale }, { translateY }], }; }); return ( {renderItem(data)} ); }; export default StackEach;