@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
JavaScript
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 };