UNPKG

@act-aks/rn-carousel

Version:

A performant, customizable carousel component for React Native with multiple animation types and TypeScript support.

218 lines (213 loc) 8.34 kB
import { jsx } from 'react/jsx-runtime'; import { createContext, useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { Dimensions, View } from 'react-native'; import Animated, { useAnimatedStyle, withSpring, withTiming, Easing, useSharedValue, useAnimatedScrollHandler } from 'react-native-reanimated'; const CarouselContext = createContext(undefined); const CarouselItem = ({ index, scrollValue, containerDimension, spacing, isHorizontal, animationType, children, itemWidth, itemHeight })=>{ const animatedStyle = useAnimatedStyle(()=>{ const progress = scrollValue.value / (containerDimension + spacing); const scale = withSpring(index === Math.round(progress) ? 1 : 0.85, { damping: 15, stiffness: 80, mass: 0.6 }); const opacity = withTiming(index === Math.round(progress) ? 1 : 0.2, { duration: 400, easing: Easing.bezier(0.4, 0, 0.2, 1) }); switch(animationType){ case 'fade': return { opacity, transform: [ { scale } ] }; case 'stack': return { transform: [ { scale }, isHorizontal ? { translateX: withSpring((index - Math.round(progress)) * 20) } : { translateY: withSpring((index - Math.round(progress)) * 20) } ] }; case 'page': return { transform: [ { perspective: 1000 }, isHorizontal ? { rotateY: withSpring(`${(index - progress) * 180}deg`, { damping: 20, stiffness: 90 }) } : { rotateX: withSpring(`${(progress - index) * 180}deg`, { damping: 20, stiffness: 90 }) } ], backfaceVisibility: 'hidden', opacity: withSpring(Math.abs(index - progress) < 0.5 ? 1 : 0, { damping: 15, stiffness: 80 }) }; case 'slide': default: return { transform: [ { scale } ] }; } }); return /*#__PURE__*/ jsx(Animated.View, { style: [ { width: isHorizontal ? itemWidth : '100%', height: isHorizontal ? '100%' : itemHeight }, animatedStyle ], children: children }); }; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); const Carousel = ({ data, renderItem, direction = 'horizontal', animationType = 'slide', itemHeight = SCREEN_HEIGHT * 0.5, itemWidth = SCREEN_WIDTH * 0.8, spacing = 10, autoPlay = false, autoPlayInterval = 3000, style })=>{ const [currentIndex, setCurrentIndex] = useState(0); const scrollViewRef = useRef(null); const scrollX = useSharedValue(0); const scrollY = useSharedValue(0); const isHorizontal = direction === 'horizontal'; const containerDimension = isHorizontal ? itemWidth : itemHeight; const scrollHandler = useAnimatedScrollHandler({ onScroll: (event)=>{ if (isHorizontal) { scrollX.value = event.contentOffset.x; } else { scrollY.value = event.contentOffset.y; } } }); const autoPlayTimer = useRef(null); const scrollToIndex = useCallback((index, animated = true)=>{ if (scrollViewRef.current) { const offset = index * (containerDimension + spacing); if (index === 0 && currentIndex === data.length - 1) { // Smooth transition from last to first slide scrollViewRef.current.scrollTo({ [isHorizontal ? 'x' : 'y']: offset, animated }); } else { scrollViewRef.current.scrollTo({ [isHorizontal ? 'x' : 'y']: offset, animated }); } } }, [ containerDimension, currentIndex, data.length, isHorizontal, spacing ]); useEffect(()=>{ if (autoPlay) { autoPlayTimer.current = setInterval(()=>{ const nextIndex = (currentIndex + 1) % data.length; scrollToIndex(nextIndex); setCurrentIndex(nextIndex); }, autoPlayInterval); } return ()=>{ if (autoPlayTimer.current) { clearInterval(autoPlayTimer.current); } }; }, [ autoPlay, autoPlayInterval, currentIndex, data.length, scrollToIndex ]); const contextValue = useMemo(()=>({ currentIndex, direction, animationType }), [ currentIndex, direction, animationType ]); return /*#__PURE__*/ jsx(CarouselContext.Provider, { value: contextValue, children: /*#__PURE__*/ jsx(View, { style: [ { flex: 1 }, style ], children: /*#__PURE__*/ jsx(Animated.ScrollView, { ref: scrollViewRef, horizontal: isHorizontal, showsHorizontalScrollIndicator: false, showsVerticalScrollIndicator: false, pagingEnabled: true, snapToInterval: containerDimension + spacing, snapToAlignment: 'center', decelerationRate: 'fast', bounces: false, bouncesZoom: false, onScroll: scrollHandler, scrollEventThrottle: 16, contentContainerStyle: { paddingHorizontal: isHorizontal ? (SCREEN_WIDTH - itemWidth) / 2 : spacing, paddingVertical: !isHorizontal ? (SCREEN_HEIGHT - itemHeight) / 2 : spacing, gap: spacing }, onMomentumScrollEnd: (event)=>{ const offset = isHorizontal ? event.nativeEvent.contentOffset.x : event.nativeEvent.contentOffset.y; const newIndex = Math.round(offset / (containerDimension + spacing)); const targetOffset = newIndex * (containerDimension + spacing); // If not perfectly centered, animate to the correct position if (offset !== targetOffset) { var _scrollViewRef_current; (_scrollViewRef_current = scrollViewRef.current) == null ? void 0 : _scrollViewRef_current.scrollTo({ [isHorizontal ? 'x' : 'y']: targetOffset, animated: true }); } setCurrentIndex(newIndex); }, children: data.map((item, index)=>/*#__PURE__*/ jsx(CarouselItem, { index: index, scrollValue: isHorizontal ? scrollX : scrollY, containerDimension: containerDimension, spacing: spacing, isHorizontal: isHorizontal, animationType: animationType, itemWidth: itemWidth, itemHeight: itemHeight, children: renderItem(item, index) }, index)) }) }) }); }; export { Carousel };