react-native-momentum-carousel
Version:
A React Native carousel component enables smooth and interactive image or content sliders with swiping capabilities. Ideal for showcasing multiple items or images in a compact space, this carousel can be customized with features like infinite scrolling, p
292 lines (274 loc) • 11.8 kB
JavaScript
;
import React, { useRef, useState, useCallback, useImperativeHandle, useEffect } from 'react';
import { View, findNodeHandle, AccessibilityInfo } from 'react-native';
import Pagination from './Pagination';
import Animated, { runOnJS, useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated';
import ItemCarousel from './ItemCarousel';
/**
* CarouselProps defines the expected properties for the CarouselMomentum component.
* - `data`: Array of items to display in the carousel.
* - `sliderWidth`: The width of the carousel container.
* - `itemWidth`: The width of each individual item in the carousel.
* - `renderItem`: Function that renders each item in the carousel.
* - `keyExtractor`: Function that provides a unique key for each item, defaulting to index if not provided.
* - `onSnap`: Callback that is triggered when an item is snapped to the center of the carousel.
* - `accessibilityLabelCarousel`: Optional accessibility label for the carousel.
* - `onMomentumScrollBegin`: Optional Callback triggered when momentum scrolling starts.
* - `onMomentumScrollEnd`: Optional Callback triggered when momentum scrolling ends.
* - `autoPlay`: Optional boolean to enable automatic scrolling through the carousel.
* - `loop`: Optional boolean to loop the carousel back to the start after reaching the last item.
* - `autoPlayInterval`: Optional number for automatic scrolling through the carousel.
* - `inactiveScale`: Optional number for scale inactive items
* - `showPagination`: Optional boolean to show pagination component.
* - `paginationStyle`: Optional style for pagination component {container:{},bullet:{},activeBullet:{}}.
* - `animation`: CarouselMomentumAnimationType Enum to choose the suitable animation.
* - `customAnimation`: Optional boolean to avoid default animation.
*/
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
export let CarouselMomentumAnimationType = /*#__PURE__*/function (CarouselMomentumAnimationType) {
CarouselMomentumAnimationType[CarouselMomentumAnimationType["Default"] = 0] = "Default";
CarouselMomentumAnimationType[CarouselMomentumAnimationType["Stack"] = 1] = "Stack";
CarouselMomentumAnimationType[CarouselMomentumAnimationType["Tinder"] = 2] = "Tinder";
return CarouselMomentumAnimationType;
}({});
/**
* CarouselMomentum component renders a horizontal scrollable carousel.
* - It supports animated transitions and snap-to-item behavior.
* - It uses `Animated.FlatList` to enable animation during scroll.
*/
const CarouselMomentum = ({
carouselStyle,
itemStyle,
data,
sliderWidth,
itemWidth,
vertical = false,
sliderHeight,
itemHeight,
renderItem,
keyExtractor,
onSnap,
accessibilityLabelCarousel,
onMomentumScrollBegin,
onMomentumScrollEnd,
autoPlay,
loop,
autoPlayInterval,
inactiveScale,
showPagination,
paginationStyle,
animation,
customAnimation,
...otherProps
}, ref) => {
if (vertical && (!sliderHeight || isNaN(sliderHeight))) {
throw 'Needed a right number value for sliderHeight';
}
if (vertical && (!itemHeight || isNaN(itemHeight))) {
throw 'Needed a right number value for itemHeight';
}
if (!vertical && (!sliderWidth || isNaN(sliderWidth))) {
throw 'Needed a right number value for sliderWidth';
}
if (!vertical && (!itemWidth || isNaN(itemWidth))) {
throw 'Needed a right number value for itemWidth';
}
// Reference to track the horizontal scroll position for animations
const scrollX = useSharedValue(0);
// State for storing the current index of the carousel
const [currentIndex, setCurrentIndex] = useState(0);
// Reference to the FlatList component for manual scroll control
const flatListRef = useRef(null);
// Reference for managing autoplay intervals
const autoplayRef = useRef(null);
// Expose imperative methods to the parent component via the `ref`
useImperativeHandle(ref, () => ({
getCurrentIndex: () => currentIndex,
// Get the current index of the carousel
goToIndex: index => goToIndex(index) // Method to scroll to a specific index
}));
/**
* handleScroll is invoked during the scroll event to update the current index.
* It also triggers the `onSnap` callback when the current index changes.
*/
const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
const offsetX = !vertical ? event.contentOffset.x : event.contentOffset.y; // Get the horizontal scroll offset
scrollX.set(offsetX); // Update the scroll position for animations
const nextIndex = Math.round(offsetX / (!vertical ? itemWidth : itemHeight)); // Calculate the current index
// If the index changes, call the onSnap callback
if (nextIndex !== currentIndex) {
runOnJS(setCurrentIndex)(nextIndex); // Update the state with the new index
runOnJS(onSnap)(nextIndex);
}
}
}, [currentIndex, itemWidth, itemHeight, onSnap, scrollX]);
/**
* Calculates the static offset of an item based on its index.
* This is used when we want to programmatically scroll to a specific item.
*/
const calculateItemOffsetStatic = useCallback(index => index * (!vertical ? itemWidth : itemHeight), [vertical, itemWidth, itemHeight]);
/**
* goToIndex scrolls to a specific index and updates the current index state.
* It also triggers the snap-to-item callback (`onSnap`).
*/
const goToIndex = useCallback(index => {
// Calculate the index with wrapping around (modulo operation)
let loopedIndex = index;
if (loop) {
loopedIndex = (index + data.length) % data.length;
}
// Ensure the FlatList reference is available before attempting to scroll
if (flatListRef.current) {
const offset = calculateItemOffsetStatic(loopedIndex); // Calculate the offset for the given index
flatListRef.current?.scrollToOffset({
animated: true,
offset
}); // Scroll to the desired offset
setCurrentIndex(loopedIndex); // Update the current index state
onSnap(loopedIndex); // Trigger the onSnap callback to notify the parent component
}
}, [loop, data.length, calculateItemOffsetStatic, onSnap]);
/**
* stopAutoplay stops the autoplay functionality by clearing the interval.
*/
const stopAutoplay = useCallback(() => {
// Stop the autoplay cycle if it is running
if (autoplayRef.current) {
clearInterval(autoplayRef.current);
autoplayRef.current = null;
}
}, []);
/**
* startAutoplay starts the autoplay functionality by setting an interval to change the index every 3 seconds.
* It only starts if autoplay is not already running.
*/
const startAutoplay = useCallback(() => {
// Start the autoplay cycle only if it's not already running
if (autoplayRef.current) {
return;
}
autoplayRef.current = setInterval(() => {
// Automatically loop to the next index and reset to 0 if at the last item
let nextIndex = 0;
if (loop) {
nextIndex = (currentIndex + 1) % data.length;
goToIndex(nextIndex);
} else {
if (currentIndex + 1 > data.length - 1) {
stopAutoplay();
} else {
nextIndex = currentIndex + 1;
goToIndex(nextIndex);
}
}
}, autoPlayInterval ? autoPlayInterval : 3000); // Advance every 3 seconds
}, [autoPlayInterval, loop, currentIndex, data.length, goToIndex, stopAutoplay]);
// UseEffect hook to start/stop autoplay based on the `autoPlay` prop
useEffect(() => {
if (autoPlay) {
// Start autoplay if enabled
startAutoplay();
} else {
// If autoplay is disabled, clear the interval
stopAutoplay();
}
return () => {
// Cleanup autoplay when component unmounts or autoPlay is turned off
stopAutoplay();
};
}, [autoPlay, startAutoplay, stopAutoplay]);
const getHandleItemInternalRef = useCallback(index => {
return _ref => {
if (index !== currentIndex || _ref === null) {
return;
}
const castedRef = _ref;
const reactTag = findNodeHandle(castedRef);
if (!reactTag) {
return;
}
AccessibilityInfo.setAccessibilityFocus(reactTag);
};
}, [currentIndex]);
/**
* keyExtractorInternal extracts a unique key for each item, either using the provided `keyExtractor`
* or falling back to the index if not provided.
*/
const keyExtractorInternal = useCallback((item, index) => keyExtractor ? keyExtractor(item, index) : index.toString(), [keyExtractor] // Recalculate if keyExtractor changes
);
/**
* renderItemInternal renders each item in the carousel with an animated scale effect.
* The scale is interpolated based on the scroll position (using scrollX) to give a zooming effect
* as items approach or leave the center of the viewport.
*/
const renderItemInternal = useCallback(info => {
return /*#__PURE__*/_jsx(ItemCarousel, {
getHandleItemInternalRef: getHandleItemInternalRef,
itemStyle: itemStyle,
renderItem: renderItem,
info: info,
itemWidth: itemWidth,
inactiveScale: inactiveScale,
scrollX: scrollX,
animation: animation,
itemHeight: itemHeight,
vertical: vertical,
customAnimation: customAnimation
});
}, [animation, customAnimation, getHandleItemInternalRef, inactiveScale, itemHeight, itemStyle, itemWidth, renderItem, scrollX, vertical] // Recalculate when these values change
);
return /*#__PURE__*/_jsxs(View, {
style: [!vertical ? {
width: sliderWidth
} : {
height: sliderHeight
}, carouselStyle],
accessibilityLabel: accessibilityLabelCarousel,
children: [/*#__PURE__*/_jsx(Animated.FlatList, {
...otherProps,
ref: flatListRef // Reference to FlatList for direct manipulation
,
data: data // The data to display in the carousel
,
keyExtractor: keyExtractor ?? keyExtractorInternal // Use the provided or internal keyExtractor
,
horizontal: !vertical // Display items horizontally
,
showsHorizontalScrollIndicator: false // Hide the scroll indicator
,
snapToInterval: !vertical ? itemWidth : itemHeight // Snapping behavior after each item
,
decelerationRate: "fast" // Fast deceleration for smooth scrolling
,
bounces: false // Disable the bounce effect on scroll edges
,
onScroll: scrollHandler // Handle scroll events
,
scrollEventThrottle: 16 // Throttle scroll event updates for smoother performance
,
onMomentumScrollEnd: onMomentumScrollEnd // Callback triggered when momentum scroll ends
,
onMomentumScrollBegin: onMomentumScrollBegin // Callback triggered when momentum scroll starts
,
renderItem: renderItemInternal // Render each item with animation
,
contentContainerStyle: !vertical ? {
paddingHorizontal: (sliderWidth - itemWidth) / 2 // Center the items within the container
} : {
paddingVertical: (sliderHeight - itemHeight) / 2
},
showsVerticalScrollIndicator: false
}), showPagination && !vertical && /*#__PURE__*/_jsx(Pagination, {
dataLength: data.length,
currentIndex: currentIndex,
paginationStyle: paginationStyle
})]
});
};
// Forward ref to the CarouselMomentum component to expose imperative methods to the parent
const WithForwardedRef = /*#__PURE__*/React.forwardRef(CarouselMomentum);
// Wrap the component with React.memo for performance optimization (prevents unnecessary re-renders)
const Memoized = /*#__PURE__*/React.memo(WithForwardedRef);
export default Memoized; // Export the memoized component
//# sourceMappingURL=CarouselMomentum.js.map