Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 88 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A highly customizable, performant carousel component for React Native with advanced animations, auto-scrolling capabilities, and infinite scrolling support. Built with React Native Reanimated for smooth, native-level performance.

**✨ Context-Based Configuration** - All carousel settings are configured through the context provider for a clean, centralized API.

## Features

✨ **Auto-scrolling** with customizable intervals
Expand Down Expand Up @@ -55,7 +57,7 @@ const Slide = ({ title, color }: { title: string; color: string }) => (

export default function BasicCarousel() {
return (
<CarouselContextProvider>
<CarouselContextProvider interval={4000} disableAutoScroll={false} initialIndex={0}>
<View style={styles.container}>
<HeroCarousel>
{slides.map((slide) => (
Expand Down Expand Up @@ -90,38 +92,50 @@ const styles = StyleSheet.create({

#### `CarouselContextProvider`

The context provider that must wrap your carousel components.
The context provider that must wrap your carousel components. **All carousel configuration is passed here.**

```tsx
<CarouselContextProvider
defaultScrollValue={1} // Initial scroll position (default: 1)
initialIndex={0} // Initial slide index (default: 0)
slideWidth={screenWidth} // Width of each slide (default: screen width)
interval={3000} // Auto-scroll interval in ms
disableAutoScroll={false} // Disable auto-scrolling
disableInfiniteScroll={false} // Disable infinite scrolling
autoScrollAnimation={(to, duration) => withTiming(to, { duration })} // Custom animation
>
{children}
</CarouselContextProvider>
```

**Props:**

| Prop | Type | Default | Description |
| ----------------------- | ------------------------------------------ | ------------ | ----------------------------------------------------------------------------------- |
| `initialIndex` | `number` | `0` | Initial slide index to start from |
| `slideWidth` | `number` | screen width | Width of each slide in pixels |
| `interval` | `number \| ((index: number) => number)` | `3000` | Auto-scroll interval in milliseconds, or function returning interval for each slide |
| `disableAutoScroll` | `boolean` | `false` | Disable automatic scrolling |
| `disableInfiniteScroll` | `boolean` | `false` | Disable infinite scrolling (shows first/last slide boundaries) |
| `autoScrollAnimation` | `(to: number, duration: number) => number` | `withTiming` | Custom animation function for auto-scroll transitions |
| `children` | `React.ReactNode` | Required | Carousel content (should contain HeroCarousel component) |

#### `HeroCarousel`

The main carousel component with auto-scrolling functionality.
The main carousel component that renders slides. **Takes no configuration props** - all configuration is handled by the context.

```tsx
<HeroCarousel
interval={3000} // Auto-scroll interval in ms
disableAutoScroll={false} // Disable auto-scrolling
goToPageAnimation={(to, duration) => withTiming(to, { duration })} // Custom page transition animation
>
{children}
<HeroCarousel>
{slides.map((slide) => (
<YourSlideComponent key={slide.id} {...slide} />
))}
</HeroCarousel>
```

**Props:**

| Prop | Type | Default | Description |
| ------------------- | --------------------------------------- | -------- | ----------------------------------------------------------------------------------- | --- |
| `interval` | `number \| ((index: number) => number)` | `3000` | Auto-scroll interval in milliseconds, or function returning interval for each slide |
| `disableAutoScroll` | `boolean` | `false` | Disable automatic scrolling | |
| `children` | `React.ReactNode[]` | Required | Array of slide components |
| Prop | Type | Description |
| ---------- | ------------------- | ------------------------- |
| `children` | `React.ReactNode[]` | Array of slide components |

### Hooks

Expand All @@ -147,7 +161,7 @@ const { scrollValue, timeoutValue, slideWidth, userInteracted, setUserInteracted
Get the current slide information and auto-scroll controls.

```tsx
const { index, total, runAutoScroll, goToPage } = useAutoCarouselSlideIndex()
const { index, total, runAutoScroll, goToPage } = useHeroCarouselSlideIndex()
```

**Returns:**
Expand Down Expand Up @@ -232,32 +246,68 @@ All pagination components automatically sync with the carousel state and support

## Advanced Usage

### Configuration Examples

Different carousel configurations using the context provider:

```tsx
// Basic auto-scrolling carousel
<CarouselContextProvider interval={3000}>
<HeroCarousel>{slides}</HeroCarousel>
</CarouselContextProvider>

// Video carousel without auto-scroll
<CarouselContextProvider disableAutoScroll>
<HeroCarousel>{videoSlides}</HeroCarousel>
</CarouselContextProvider>

// Carousel with custom intervals per slide
<CarouselContextProvider interval={(index) => (index + 1) * 2000}>
<HeroCarousel>{slides}</HeroCarousel>
</CarouselContextProvider>

// Carousel starting from specific slide
<CarouselContextProvider initialIndex={2} disableInfiniteScroll>
<HeroCarousel>{slides}</HeroCarousel>
</CarouselContextProvider>

// Custom slide width and animation
<CarouselContextProvider
slideWidth={300}
autoScrollAnimation={(to, duration) => withSpring(to, { damping: 15 })}
>
<HeroCarousel>{slides}</HeroCarousel>
</CarouselContextProvider>
```

### Programmatic Navigation

Control the carousel programmatically using the context:

```tsx
const CarouselWithControls = () => {
const { scrollValue } = useCarouselContext()
const { runAutoScroll } = useAutoCarouselSlideIndex()
const { scrollValue, goToPage } = useCarouselContext()
const { runAutoScroll } = useHeroCarouselSlideIndex()

const goToNext = () => {
runAutoScroll(0) // Immediate transition
}

const goToSlide = (slideIndex: number) => {
scrollValue.value = withTiming(slideIndex, { duration: 500 })
goToPage(slideIndex, 500) // Go to slide with 500ms animation
}

return (
<View>
<HeroCarousel disableAutoScroll>{/* Your slides */}</HeroCarousel>

<View style={styles.controls}>
<Button title="Previous" onPress={() => goToSlide(scrollValue.value - 1)} />
<Button title="Next" onPress={goToNext} />
<CarouselContextProvider disableAutoScroll>
<View>
<HeroCarousel>{/* Your slides */}</HeroCarousel>

<View style={styles.controls}>
<Button title="Previous" onPress={() => goToSlide(scrollValue.value - 1)} />
<Button title="Next" onPress={goToNext} />
</View>
</View>
</View>
</CarouselContextProvider>
)
}
```
Expand All @@ -281,6 +331,18 @@ useEffect(() => {
}, [])
```

## Architecture

### Context-Based Configuration

This library uses a **context-based architecture** where all carousel configuration is passed to the `CarouselContextProvider` rather than individual components. This design provides several benefits:

✅ **Centralized Configuration** - All settings in one place
✅ **Cleaner Component API** - Components focus on rendering, not configuration
✅ **Easier Testing** - Mock context for isolated component testing

That allows for components like pagination to not be attached to the carousel component.

## Troubleshooting

### Common Issues
Expand Down
6 changes: 3 additions & 3 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ Example template:

```tsx
// examples/YourExample.tsx
import { AutoCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
import { HeroCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'

export default function YourExample() {
return (
<CarouselContextProvider>
<AutoCarousel>{/* Your slides */}</AutoCarousel>
<CarouselContextProvider interval={3000} disableAutoScroll={false} initialIndex={0}>
<HeroCarousel>{/* Your slides */}</HeroCarousel>
</CarouselContextProvider>
)
}
Expand Down
1 change: 0 additions & 1 deletion example/examples/BasicExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
transformOrigin: 'center',
transform: [{ scale: 1.6 }],
},
gradient: {
position: 'absolute',
Expand Down
1 change: 0 additions & 1 deletion example/examples/EnteringAnimationExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
transformOrigin: 'center',
transform: [{ scale: 1.6 }],
},
gradient: {
position: 'absolute',
Expand Down
1 change: 0 additions & 1 deletion example/examples/OffsetExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
transformOrigin: 'center',
transform: [{ scale: 1.6 }],
},
gradient: {
position: 'absolute',
Expand Down
5 changes: 2 additions & 3 deletions example/examples/TimerPaginationExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ export default function TimerPaginationExample() {
}

return (
<CarouselContextProvider>
<CarouselContextProvider interval={getInterval}>
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<HeroCarousel interval={getInterval}>
<HeroCarousel>
{images.map((image, index) => (
<Slide
key={index}
Expand Down Expand Up @@ -93,7 +93,6 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
transformOrigin: 'center',
transform: [{ scale: 1.6 }],
},
gradient: {
position: 'absolute',
Expand Down
4 changes: 2 additions & 2 deletions example/examples/VideoCarouselExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; in

export default function VideoCarouselExample() {
return (
<CarouselContextProvider>
<CarouselContextProvider disableAutoScroll={true}>
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<HeroCarousel disableAutoScroll={true}>
<HeroCarousel>
{videos.map((video, index) => (
<Slide key={index} videoUri={video} title={videoTitles[index]} index={index} />
))}
Expand Down
21 changes: 16 additions & 5 deletions src/components/AnimatedPagedView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { forwardRef, useImperativeHandle } from 'react'
import React, { forwardRef, useCallback, useImperativeHandle } from 'react'
import { Dimensions } from 'react-native'
import Animated, {
useSharedValue,
useAnimatedStyle,
useAnimatedReaction,
runOnJS,
withTiming,
clamp,
} from 'react-native-reanimated'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'

Expand All @@ -27,14 +28,23 @@ export const AnimatedPagedView = forwardRef<AnimatedPagedScrollViewRef, Animated
(props, ref) => {
const translateX = useSharedValue(0)
const context = useSharedValue({ x: 0 })
const childrenArray = React.Children.toArray(props.children)

const clampValue = useCallback(
(value: number) => {
'worklet'
return clamp(value, 0, (childrenArray.length - 1) * SCREEN_WIDTH)
},
[childrenArray.length],
)

const gesture = Gesture.Pan()
.onStart(() => {
context.value = { x: translateX.value }
runOnJS(props.onScrollBeginDrag)()
})
.onUpdate((event) => {
translateX.value = context.value.x - event.translationX
translateX.value = clampValue(context.value.x - event.translationX)
})
.onEnd((event) => {
const velocity = event.velocityX
Expand All @@ -43,15 +53,16 @@ export const AnimatedPagedView = forwardRef<AnimatedPagedScrollViewRef, Animated
velocity > 500 ? currentPage - 1 : velocity < -500 ? currentPage + 1 : currentPage
// in case the gesture overshoots, snap to the nearest page
if (Math.abs(context.value.x - translateX.value) > SCREEN_WIDTH / 2) {
translateX.value = withTiming(currentPage * SCREEN_WIDTH)
translateX.value = withTiming(clampValue(currentPage * SCREEN_WIDTH))
} else {
translateX.value = withTiming(targetPage * SCREEN_WIDTH)
translateX.value = withTiming(clampValue(targetPage * SCREEN_WIDTH))
}
})

const animatedStyle = useAnimatedStyle(() => {
const clampedTranslateX = clampValue(translateX.value)
return {
transform: [{ translateX: -translateX.value }],
transform: [{ translateX: -clampedTranslateX }],
}
}, [])

Expand Down
33 changes: 18 additions & 15 deletions src/components/HeroCarousel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,35 @@
import React from 'react'

import { DEFAULT_INTERVAL } from './index.preset'
import { useCarouselContext } from '../../context/CarouselContext'
import { HeroCarouselSlide } from '../HeroCarouselSlide'
import { HeroCarouselAdapter } from '../AnimatedPagedView/Adapter'
import { useAutoScroll } from '../../hooks/useAutoScroll'
import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'
import { DEFAULT_ANIMATION } from '../../hooks/useManualScroll'
import { DEFAULT_INTERVAL } from './index.preset'

export type HeroCarouselProps = {
interval?: number | ((index: number) => number)
children: React.ReactNode[]
autoScrollAnimation?: (to: number, duration: number) => number
disableAutoScroll?: boolean
}

export const HeroCarousel = ({
interval = DEFAULT_INTERVAL,
children,
disableAutoScroll = false,
autoScrollAnimation = DEFAULT_ANIMATION,
}: HeroCarouselProps) => {
const { scrollValue, userInteracted, slideWidth, timeoutValue, goToPage, manualScrollValue } =
useCarouselContext()
export const HeroCarousel = ({ children }: HeroCarouselProps) => {
const {
scrollValue,
userInteracted,
slideWidth,
timeoutValue,
goToPage,
manualScrollValue,
disableInfiniteScroll,
interval,
disableAutoScroll,
autoScrollAnimation,
} = useCarouselContext()
const { paddedChildrenArray } = useInfiniteScroll({
children,
slideWidth,
goToPage,
scrollValue,
disabled: disableInfiniteScroll,
})

const autoScrollEnabled = !userInteracted
Expand All @@ -36,12 +38,13 @@ export const HeroCarousel = ({
scrollValue,
slideWidth,
autoScrollEnabled,
disableAutoScroll,
interval,
disableAutoScroll: disableAutoScroll ?? false,
interval: interval ?? DEFAULT_INTERVAL,
goToPage: (page: number, duration?: number) => {
goToPage(page, duration, autoScrollAnimation)
},
timeoutValue,
totalLength: paddedChildrenArray.length,
})

return (
Expand Down
Loading
Loading