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
3 changes: 2 additions & 1 deletion example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
]
],
"expo-video"
],
"experiments": {
"typedRoutes": true
Expand Down
10 changes: 10 additions & 0 deletions example/app/(examples)/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ const examples = [
description: 'Carousel with entering/exiting animations triggered by shared values',
route: '/entering-animation' as const,
},
{
title: 'Timer Pagination Carousel',
description: 'Timer-based pagination with auto-slide progress indicator',
route: '/timer-pagination' as const,
},
{
title: 'Video Carousel',
description: 'A carousel showcasing videos using expo-video',
route: '/video-carousel' as const,
},
]

export default function HomeScreen() {
Expand Down
3 changes: 3 additions & 0 deletions example/app/(examples)/timer-pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TimerPaginationExample from '@/examples/TimerPaginationExample'

export default TimerPaginationExample
3 changes: 3 additions & 0 deletions example/app/(examples)/video-carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import VideoCarouselExample from '@/examples/VideoCarouselExample'

export default VideoCarouselExample
7 changes: 5 additions & 2 deletions example/examples/EnteringAnimationExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n

return (
<View key={index} style={styles.slide}>
<Image key={image} source={{ uri: image }} style={styles.image} contentFit="cover" />
<Image source={{ uri: image }} style={styles.image} contentFit="cover" />
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.8)']} style={styles.gradient}>
<SlideAnimatedView {...animationConfig}>
<SlideAnimatedView style={styles.textContainer} {...animationConfig}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>
Animation: {animationNames[index % animationNames.length]}
Expand Down Expand Up @@ -68,6 +68,9 @@ export default function EnteringAnimationExample() {
}

const styles = StyleSheet.create({
textContainer: {
flex: 1,
},
container: {
flex: 1,
backgroundColor: '#fff',
Expand Down
174 changes: 174 additions & 0 deletions example/examples/TimerPaginationExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
AutoCarousel,
CarouselContextProvider,
useAutoCarouselSlideIndex,
} 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 { useEffect } from 'react'
import { BlurView } from 'expo-blur'
import { TimerPagination } from './components/TimerPagination'

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,
getInterval,
}: {
image: string
title: string
index: number
getInterval: (index: number) => number
}) => {
const { index: currentIndex } = useAutoCarouselSlideIndex()
const interval = getInterval(currentIndex)

return (
<View style={styles.slide}>
<Image key={image} source={{ uri: image }} style={styles.image} contentFit="cover" />
<LinearGradient colors={['rgba(0,0,0,0.8)', 'transparent']} style={styles.topGradient} />
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.8)']} style={styles.gradient}>
<BlurView style={styles.blurView}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>Slide change interval: {interval / 1000} s</Text>
</BlurView>
</LinearGradient>
</View>
)
}

export default function TimerPaginationExample() {
// Preload all images when component mounts
useEffect(() => {
Image.prefetch(images)
}, [])

const getInterval = (index: number) => {
'worklet'
return index * 3000
}

return (
<CarouselContextProvider>
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<AutoCarousel interval={getInterval}>
{images.map((image, index) => (
<Slide
key={index}
image={image}
title={`Slide ${index + 1}`}
index={index}
getInterval={getInterval}
/>
))}
</AutoCarousel>
<TimerPagination total={images.length} hideProgressOnInteraction />
</View>
</SafeAreaView>
</CarouselContextProvider>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
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,
},
topGradient: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '20%',
},
title: {
fontSize: 32,
lineHeight: 32,
fontWeight: 'bold',
color: 'white',
},
subtitle: {
fontSize: 16,
lineHeight: 16,
fontWeight: '500',
color: 'white',
opacity: 0.8,
},
paginationContainer: {
position: 'absolute',
top: 20,
left: 20,
right: 20,
overflow: 'hidden',
flexDirection: 'row',
gap: 8,
borderRadius: 16,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
zIndex: 10,
},
paginationDot: {
flex: 1,
height: 3,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 2,
overflow: 'hidden',
},
dotBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.5)',
},
dotProgress: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: 'white',
borderRadius: 2,
},
blurView: {
position: 'absolute',
bottom: 20,
padding: 20,
margin: 8,
borderRadius: 16,
gap: 8,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
overflow: 'hidden',
},
})
158 changes: 158 additions & 0 deletions example/examples/VideoCarouselExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {
AutoCarousel,
CarouselContextProvider,
useAutoCarouselSlideIndex,
} from '@strv/react-native-hero-carousel'
import { SafeAreaView, StyleSheet, View, Text, Pressable, Dimensions, Platform } from 'react-native'
import { useVideoPlayer, VideoView } from 'expo-video'
import { LinearGradient } from 'expo-linear-gradient'
import { useActiveSlideEffect, useIsActiveSlide } from '@/hooks/useActiveSlideEffect'
import { useEffect, useRef, useState } from 'react'
import { TimerPagination } from './components/TimerPagination'
import { useEvent, useEventListener } from 'expo'

const { width, height } = Dimensions.get('window')
// Sample video URLs - these are publicly available videos that work well for testing
const videos = [
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
]

const videoTitles = [
'For Bigger Blazes',
'For Bigger Escapes',
'For Bigger Fun',
'Big Buck Bunny',
'Elephants Dream',
]

const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; index: number }) => {
const player = useVideoPlayer(videoUri)
const { runAutoScroll } = useAutoCarouselSlideIndex()
const isActiveSlide = useIsActiveSlide()
const [duration, setDuration] = useState(0)
useActiveSlideEffect(() => {
player.currentTime = 0
player.play()
return () => {
player.pause()
}
})

const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing })

useEventListener(player, 'statusChange', ({ status }) => {
if (status === 'readyToPlay') {
setDuration(player.duration)
}
})

const intervalRef = useRef<ReturnType<typeof runAutoScroll> | null>(null)

useEffect(() => {
if (isActiveSlide && duration) {
intervalRef.current = runAutoScroll(duration * 1000)
}
}, [isActiveSlide, duration, runAutoScroll])

return (
<View style={styles.slide}>
<Pressable
key={index}
style={styles.slide}
onPress={() => {
if (isPlaying) {
player.pause()
intervalRef.current?.pause()
} else {
player.play()
intervalRef.current?.resume()
}
}}
>
<VideoView
player={player}
style={styles.video}
contentFit={Platform.OS === 'android' ? 'fill' : 'cover'}
nativeControls={false}
/>
<LinearGradient colors={['rgba(0,0,0,0.8)', 'transparent']} style={styles.topGradient} />
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.8)']} style={styles.gradient}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>Swipe to navigate • Tap to play/pause</Text>
</LinearGradient>
</Pressable>
</View>
)
}

export default function VideoCarouselExample() {
return (
<CarouselContextProvider>
<SafeAreaView style={styles.container}>
<View style={styles.container}>
<AutoCarousel disableAutoScroll={true}>
{videos.map((video, index) => (
<Slide key={index} videoUri={video} title={videoTitles[index]} index={index} />
))}
</AutoCarousel>
<TimerPagination total={videos.length} hideProgressOnInteraction={false} />
</View>
</SafeAreaView>
</CarouselContextProvider>
)
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
slide: {
flex: 1,
width: '100%',
height: '100%',
overflow: 'hidden',
},
video: {
width: width,
height: height,
},
gradient: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '50%',
justifyContent: 'flex-end',
padding: 20,
},
topGradient: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '20%',
},
title: {
fontSize: 32,
bottom: 60,
left: 20,
position: 'absolute',
lineHeight: 32,
fontWeight: 'bold',
color: 'white',
},
subtitle: {
fontSize: 16,
bottom: 20,
left: 20,
position: 'absolute',
lineHeight: 20,
color: 'white',
opacity: 0.8,
},
})
Loading
Loading