diff --git a/example/app.json b/example/app.json
index 09747de..021dca1 100644
--- a/example/app.json
+++ b/example/app.json
@@ -6,11 +6,12 @@
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "example",
- "userInterfaceStyle": "automatic",
+ "userInterfaceStyle": "dark",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
- "bundleIdentifier": "com.petrkonecny2.example"
+ "bundleIdentifier": "com.petrkonecny2.example",
+ "appleTeamId": "EAVQTDVRT7"
},
"android": {
"adaptiveIcon": {
diff --git a/example/app/(examples)/_layout.tsx b/example/app/(examples)/_layout.tsx
new file mode 100644
index 0000000..42bc691
--- /dev/null
+++ b/example/app/(examples)/_layout.tsx
@@ -0,0 +1,32 @@
+import { Stack } from 'expo-router'
+
+export default function ExamplesLayout() {
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/example/app/(examples)/animated.tsx b/example/app/(examples)/animated.tsx
new file mode 100644
index 0000000..736c14d
--- /dev/null
+++ b/example/app/(examples)/animated.tsx
@@ -0,0 +1,3 @@
+import AnimatedExample from '@/examples/AnimatedExample'
+
+export default AnimatedExample
diff --git a/example/app/(examples)/basic.tsx b/example/app/(examples)/basic.tsx
new file mode 100644
index 0000000..9e2bb09
--- /dev/null
+++ b/example/app/(examples)/basic.tsx
@@ -0,0 +1,3 @@
+import BasicExample from '@/examples/BasicExample'
+
+export default BasicExample
diff --git a/example/app/(examples)/index.tsx b/example/app/(examples)/index.tsx
new file mode 100644
index 0000000..197a596
--- /dev/null
+++ b/example/app/(examples)/index.tsx
@@ -0,0 +1,75 @@
+import { ThemedText } from '@/components/ThemedText'
+import { ThemedView } from '@/components/ThemedView'
+import { StyleSheet, TouchableOpacity, ScrollView } from 'react-native'
+import { Link } from 'expo-router'
+import { SafeAreaView } from 'react-native-safe-area-context'
+const examples = [
+ {
+ title: 'Basic Carousel',
+ description: 'A simple carousel with basic animations and pagination',
+ route: '/basic' as const,
+ },
+ {
+ title: 'Animated Carousel',
+ description: 'Advanced animations with custom transitions',
+ route: '/animated' as const,
+ },
+]
+
+export default function HomeScreen() {
+ return (
+
+
+
+ Carousel Examples
+
+
+ Select an example to view
+
+
+ {examples.map((example) => (
+
+
+ {example.title}
+ {example.description}
+
+
+ ))}
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: 'black',
+ },
+ scrollView: {
+ flex: 1,
+ padding: 16,
+ },
+ title: {
+ textAlign: 'center',
+ },
+ subtitle: {
+ textAlign: 'center',
+ opacity: 0.7,
+ },
+ card: {
+ padding: 16,
+ borderRadius: 12,
+ marginBottom: 16,
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ },
+ cardTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ },
+ cardDescription: {
+ fontSize: 14,
+ opacity: 0.7,
+ },
+})
diff --git a/example/app/(tabs)/_layout.tsx b/example/app/(tabs)/_layout.tsx
deleted file mode 100644
index 5f83c98..0000000
--- a/example/app/(tabs)/_layout.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Tabs } from 'expo-router'
-import React from 'react'
-import { Platform } from 'react-native'
-
-import { HapticTab } from '@/components/HapticTab'
-import { IconSymbol } from '@/components/ui/IconSymbol'
-import TabBarBackground from '@/components/ui/TabBarBackground'
-import { Colors } from '@/constants/Colors'
-import { useColorScheme } from '@/hooks/useColorScheme'
-
-export default function TabLayout() {
- const colorScheme = useColorScheme()
-
- return (
-
- ,
- }}
- />
- ,
- }}
- />
-
- )
-}
diff --git a/example/app/(tabs)/explore.tsx b/example/app/(tabs)/explore.tsx
deleted file mode 100644
index d0dfdd5..0000000
--- a/example/app/(tabs)/explore.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { ThemedView } from '@/components/ThemedView'
-import { Carousel } from '@strv/react-native-hero-carousel'
-import { SafeAreaView } from 'react-native'
-export default function TabTwoScreen() {
- return (
-
-
-
-
-
- )
-}
diff --git a/example/app/(tabs)/index.tsx b/example/app/(tabs)/index.tsx
deleted file mode 100644
index 7f867b8..0000000
--- a/example/app/(tabs)/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { ThemedText } from '@/components/ThemedText'
-import { ThemedView } from '@/components/ThemedView'
-import { AutoCarousel } from '@strv/react-native-hero-carousel'
-import { SafeAreaView, StyleSheet, Dimensions } from 'react-native'
-import { Image } from 'expo-image'
-import { LinearGradient } from 'expo-linear-gradient'
-
-const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
-
-const getRandomImageUrl = () => {
- return `https://picsum.photos/${SCREEN_WIDTH}/${SCREEN_HEIGHT}?random=${Math.floor(Math.random() * 1000)}`
-}
-
-const images = Array.from({ length: 5 }, getRandomImageUrl)
-
-export default function HomeScreen() {
- return (
-
-
-
- {images.map((image, index) => (
-
-
-
- Slide {index + 1}
-
-
- ))}
-
-
-
- )
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- slide: {
- flex: 1,
- width: SCREEN_WIDTH,
- height: SCREEN_HEIGHT * 0.7,
- },
- image: {
- width: '100%',
- height: '100%',
- },
- gradient: {
- position: 'absolute',
- bottom: 0,
- left: 0,
- right: 0,
- height: '50%',
- justifyContent: 'flex-end',
- padding: 20,
- },
- title: {
- fontSize: 32,
- bottom: 100,
- left: 20,
- position: 'absolute',
- lineHeight: 32,
- fontWeight: 'bold',
- color: 'white',
- },
-})
diff --git a/example/app/+not-found.tsx b/example/app/+not-found.tsx
index b7601ca..dea041f 100644
--- a/example/app/+not-found.tsx
+++ b/example/app/+not-found.tsx
@@ -8,7 +8,7 @@ import { ThemedView } from '@/components/ThemedView'
export default function NotFoundScreen() {
return (
<>
-
+
This screen does not exist.
diff --git a/example/app/_layout.tsx b/example/app/_layout.tsx
index 5ae251d..82ddcbf 100644
--- a/example/app/_layout.tsx
+++ b/example/app/_layout.tsx
@@ -1,31 +1,19 @@
-import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
-import { useFonts } from 'expo-font'
import { Stack } from 'expo-router'
-import { StatusBar } from 'expo-status-bar'
-import 'react-native-reanimated'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
-import { useColorScheme } from '@/hooks/useColorScheme'
+import { initialWindowMetrics, SafeAreaProvider } from 'react-native-safe-area-context'
+import * as SystemUI from 'expo-system-ui'
+import { DarkTheme, ThemeProvider } from '@react-navigation/native'
-export default function RootLayout() {
- const colorScheme = useColorScheme()
- const [loaded] = useFonts({
- SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
- })
-
- if (!loaded) {
- // Async font loading only occurs in development.
- return null
- }
+SystemUI.setBackgroundColorAsync('black')
+export default function RootLayout() {
return (
-
-
-
-
-
-
-
+
+
+
+
+
-
+
)
}
diff --git a/example/examples/AnimatedExample.tsx b/example/examples/AnimatedExample.tsx
new file mode 100644
index 0000000..aed7710
--- /dev/null
+++ b/example/examples/AnimatedExample.tsx
@@ -0,0 +1,117 @@
+import {
+ AutoCarousel,
+ interpolateInsideCarousel,
+ useCarouselContext,
+ useAutoCarouselSlideIndex,
+ CarouselContextProvider,
+} from '@strv/react-native-hero-carousel'
+import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
+import { Image } from 'expo-image'
+import { LinearGradient } from 'expo-linear-gradient'
+import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
+import { Pagination } from './components/Pagination'
+
+const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
+
+const getRandomImageUrl = () => {
+ return `https://picsum.photos/${SCREEN_WIDTH}/${SCREEN_HEIGHT}?random=${Math.floor(Math.random() * 1000)}`
+}
+
+const images = Array.from({ length: 5 }, getRandomImageUrl)
+
+const Slide = ({ image, title, index }: { image: string; title: string; index: number }) => {
+ const { scrollValue } = useCarouselContext()
+ const { index: slideIndex, total } = useAutoCarouselSlideIndex()
+
+ const rStyle = useAnimatedStyle(() => {
+ const progress = interpolateInsideCarousel(scrollValue.value, slideIndex, total, {
+ slideBefore: 0,
+ thisSlide: 1,
+ slideAfter: 0,
+ offset: 0.2,
+ })
+
+ return {
+ flex: 1,
+ width: '100%',
+ height: '100%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ transformOrigin: 'center',
+ transform: [
+ {
+ scale: interpolate(progress, [0, 1], [0.8, 1], Extrapolation.CLAMP),
+ },
+ {
+ rotate: `${interpolate(progress, [0, 1], [-15, 0], Extrapolation.CLAMP)}deg`,
+ },
+ ],
+ opacity: progress,
+ }
+ })
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ )
+}
+
+export default function AnimatedExample() {
+ return (
+
+
+
+
+ {images.map((image, index) => (
+
+ ))}
+
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ slide: {
+ flex: 1,
+ width: '100%',
+ height: '100%',
+ overflow: 'hidden',
+ },
+ image: {
+ width: '100%',
+ height: '100%',
+ transformOrigin: 'center',
+ transform: [{ scale: 1.6 }],
+ },
+ gradient: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: '50%',
+ justifyContent: 'flex-end',
+ padding: 20,
+ },
+ title: {
+ fontSize: 32,
+ bottom: 100,
+ left: 20,
+ position: 'absolute',
+ lineHeight: 32,
+ fontWeight: 'bold',
+ color: 'white',
+ },
+})
diff --git a/example/examples/BasicExample.tsx b/example/examples/BasicExample.tsx
new file mode 100644
index 0000000..e74da3e
--- /dev/null
+++ b/example/examples/BasicExample.tsx
@@ -0,0 +1,76 @@
+import { AutoCarousel, CarouselContextProvider } from '@strv/react-native-hero-carousel'
+import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
+import { Image } from 'expo-image'
+import { LinearGradient } from 'expo-linear-gradient'
+
+const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
+
+const getRandomImageUrl = () => {
+ return `https://picsum.photos/${SCREEN_WIDTH}/${SCREEN_HEIGHT}?random=${Math.floor(Math.random() * 1000)}`
+}
+
+const images = Array.from({ length: 5 }, getRandomImageUrl)
+
+const Slide = ({ image, title, index }: { image: string; title: string; index: number }) => {
+ return (
+
+
+
+ {title}
+
+
+ )
+}
+
+export default function BasicExample() {
+ return (
+
+
+
+
+ {images.map((image, index) => (
+
+ ))}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#fff',
+ },
+ slide: {
+ flex: 1,
+ width: '100%',
+ height: '100%',
+ overflow: 'hidden',
+ },
+ image: {
+ width: '100%',
+ height: '100%',
+ transformOrigin: 'center',
+ transform: [{ scale: 1.6 }],
+ },
+ gradient: {
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ height: '50%',
+ justifyContent: 'flex-end',
+ padding: 20,
+ },
+ title: {
+ fontSize: 32,
+ bottom: 100,
+ left: 20,
+ position: 'absolute',
+ lineHeight: 32,
+ fontWeight: 'bold',
+ color: 'white',
+ },
+})
diff --git a/example/examples/components/CarouselBase.tsx b/example/examples/components/CarouselBase.tsx
new file mode 100644
index 0000000..139710c
--- /dev/null
+++ b/example/examples/components/CarouselBase.tsx
@@ -0,0 +1,28 @@
+import {
+ AutoCarousel,
+ AutoCarouselProps,
+ CarouselContextProvider,
+} from '@strv/react-native-hero-carousel'
+import { SafeAreaView, StyleSheet, View } from 'react-native'
+import { Stack } from 'expo-router'
+import { Pagination } from '@/examples/components/Pagination'
+
+export function CarouselBase({ children }: { children: AutoCarouselProps['children'] }) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+})
diff --git a/example/examples/components/Pagination.tsx b/example/examples/components/Pagination.tsx
new file mode 100644
index 0000000..9ed1fd2
--- /dev/null
+++ b/example/examples/components/Pagination.tsx
@@ -0,0 +1,56 @@
+import { useCarouselContext } from '@strv/react-native-hero-carousel'
+import { StyleSheet, View } from 'react-native'
+import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
+import { useSafeAreaInsets } from 'react-native-safe-area-context'
+
+const PaginationDot = ({ index }: { index: number }) => {
+ const { scrollValue } = useCarouselContext()
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ width: interpolate(
+ scrollValue.value - 1,
+ [index - 1, index, index + 1],
+ [8, 24, 8],
+ Extrapolation.CLAMP,
+ ),
+ opacity: interpolate(
+ scrollValue.value - 1,
+ [index - 1, index, index + 1],
+ [0.5, 1, 0.5],
+ Extrapolation.CLAMP,
+ ),
+ }
+ })
+
+ return
+}
+
+export const Pagination = ({ total }: { total: number }) => {
+ const { bottom } = useSafeAreaInsets()
+
+ return (
+
+ {Array.from({ length: total }).map((_, index) => (
+
+ ))}
+
+ )
+}
+
+const styles = StyleSheet.create({
+ pagination: {
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: 8,
+ },
+ dot: {
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: 'white',
+ },
+})
diff --git a/example/examples/utils.ts b/example/examples/utils.ts
new file mode 100644
index 0000000..4ab3bf5
--- /dev/null
+++ b/example/examples/utils.ts
@@ -0,0 +1,10 @@
+import { Dimensions } from 'react-native'
+
+export const SCREEN_WIDTH = Dimensions.get('window').width
+export const SCREEN_HEIGHT = Dimensions.get('window').height
+
+const getRandomImageUrl = () => {
+ return `https://picsum.photos/${SCREEN_WIDTH}/${SCREEN_HEIGHT}?random=${Math.floor(Math.random() * 1000)}`
+}
+
+export const SLIDES = Array.from({ length: 5 }, getRandomImageUrl)
diff --git a/src/components/AnimatedPagedView/styles.ts b/src/components/AnimatedPagedView/styles.ts
index 93e6461..8685d74 100644
--- a/src/components/AnimatedPagedView/styles.ts
+++ b/src/components/AnimatedPagedView/styles.ts
@@ -4,6 +4,7 @@ export const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
+ overflow: 'hidden',
},
contentContainer: {
flexDirection: 'row',
diff --git a/src/components/AutoCarousel/index.preset.ts b/src/components/AutoCarousel/index.preset.ts
index 15acf85..26e0b5e 100644
--- a/src/components/AutoCarousel/index.preset.ts
+++ b/src/components/AutoCarousel/index.preset.ts
@@ -1,4 +1,4 @@
-export const TRANSITION_DURATION = 600
+export const TRANSITION_DURATION = 1000
export const ROUNDING_PRECISION = 0.003
export const ANDROID_FALLBACK_DURATION = 0.00000000001
export const DEFAULT_INTERVAL = 5000
diff --git a/src/components/AutoCarousel/index.tsx b/src/components/AutoCarousel/index.tsx
index da6db40..ae61584 100644
--- a/src/components/AutoCarousel/index.tsx
+++ b/src/components/AutoCarousel/index.tsx
@@ -2,20 +2,17 @@ import React, { useRef, useEffect, useCallback } from 'react'
import { runOnJS, useSharedValue, withTiming, useAnimatedReaction } from 'react-native-reanimated'
import { DEFAULT_INTERVAL, ROUNDING_PRECISION, TRANSITION_DURATION } from './index.preset'
-import { CarouselContextProvider, useCarouselContext } from '../../context/CarouselContext'
+import { useCarouselContext } from '../../context/CarouselContext'
import { AutoCarouselSlide } from '../AutoCarouselSlide'
import { customRound } from '../../utils/round'
import { AutoCarouselAdapter } from '../AnimatedPagedView/Adapter'
-type AutoCarouselProps = {
+export type AutoCarouselProps = {
interval?: number
- children: JSX.Element | JSX.Element[]
+ children: JSX.Element[]
}
-export const AutoCarouselWithoutProvider = ({
- interval = DEFAULT_INTERVAL,
- children,
-}: AutoCarouselProps) => {
+export const AutoCarousel = ({ interval = DEFAULT_INTERVAL, children }: AutoCarouselProps) => {
const { scrollValue, userInteracted, slideWidth } = useCarouselContext()
const offset = useSharedValue({ value: slideWidth })
@@ -95,26 +92,25 @@ export const AutoCarouselWithoutProvider = ({
)
return (
- {
- 'worklet'
- scrollValue.value = activeIndex
- }}
- >
- {React.Children.map(paddedChildrenArray, (child, index) => (
-
- {child}
-
- ))}
-
- )
-}
-
-export const AutoCarousel = ({ interval, children }: AutoCarouselProps) => {
- return (
-
- {children}
-
+ <>
+ {
+ 'worklet'
+ scrollValue.value = activeIndex
+ }}
+ >
+ {React.Children.map(paddedChildrenArray, (child, index) => (
+
+ {child}
+
+ ))}
+
+ >
)
}
diff --git a/src/components/AutoCarouselSlide/index.tsx b/src/components/AutoCarouselSlide/index.tsx
index 8fc1e8f..cf80ada 100644
--- a/src/components/AutoCarouselSlide/index.tsx
+++ b/src/components/AutoCarouselSlide/index.tsx
@@ -1,11 +1,22 @@
import { View } from 'react-native'
+import { AutoCarouselSlideContext } from '../../context/SlideContext'
export const AutoCarouselSlide = ({
children,
width,
+ index,
+ total,
}: {
children: React.ReactNode
width: number
+ index: number
+ total: number
}) => {
- return {children}
+ return (
+
+
+ {children}
+
+
+ )
}
diff --git a/src/context/SlideContext/index.tsx b/src/context/SlideContext/index.tsx
new file mode 100644
index 0000000..13531e0
--- /dev/null
+++ b/src/context/SlideContext/index.tsx
@@ -0,0 +1,11 @@
+import { createContext, useContext } from 'react'
+
+export const AutoCarouselSlideContext = createContext<{ index: number; total: number } | null>(null)
+
+export const useAutoCarouselSlideIndex = () => {
+ const context = useContext(AutoCarouselSlideContext)
+ if (!context) {
+ throw new Error('useAutoCarouselSlideIndex must be used within a AutoCarouselSlide')
+ }
+ return context
+}
diff --git a/src/context/index.tsx b/src/context/index.tsx
new file mode 100644
index 0000000..93a6354
--- /dev/null
+++ b/src/context/index.tsx
@@ -0,0 +1,2 @@
+export * from './CarouselContext'
+export * from './SlideContext'
diff --git a/src/index.tsx b/src/index.tsx
index cb64ac1..1710fbf 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1 +1,3 @@
export * from './components'
+export * from './context'
+export * from './utils'
diff --git a/src/utils/index.tsx b/src/utils/index.tsx
new file mode 100644
index 0000000..9ab9f57
--- /dev/null
+++ b/src/utils/index.tsx
@@ -0,0 +1,2 @@
+export * from './interpolateInsideCarousel'
+export * from './round'
diff --git a/src/utils/interpolateInsideCarousel.ts b/src/utils/interpolateInsideCarousel.ts
new file mode 100644
index 0000000..1cdaa59
--- /dev/null
+++ b/src/utils/interpolateInsideCarousel.ts
@@ -0,0 +1,39 @@
+import { Extrapolation, interpolate } from 'react-native-reanimated'
+
+export const interpolateInsideCarousel = (
+ scrollValue: number,
+ slideIndex: number,
+ totalLength: number,
+ values: {
+ slideBefore: number
+ thisSlide: number
+ slideAfter: number
+ offset: number
+ },
+) => {
+ 'worklet'
+ const { offset, slideBefore: incoming, thisSlide: inside, slideAfter: outgoing } = values
+ let adjustedIndex = slideIndex
+
+ if (slideIndex === 0) {
+ adjustedIndex = Math.max(totalLength - 2, 0)
+ }
+
+ if (slideIndex === totalLength - 1) {
+ adjustedIndex = 1
+ }
+
+ const inputRange = [
+ 0,
+ Math.min(1, adjustedIndex - 1) - offset,
+ adjustedIndex - 1 + offset,
+ adjustedIndex,
+ adjustedIndex + 1 - offset,
+ Math.max(totalLength - 2, adjustedIndex + 1) + offset,
+ totalLength - 1,
+ ]
+
+ const outputValues = [inside, incoming, outgoing, inside, incoming, outgoing, inside]
+
+ return interpolate(scrollValue, inputRange, outputValues, Extrapolation.CLAMP)
+}