Skip to content

Commit 6c04a6a

Browse files
petrkonecny2Petr Konecny
andauthored
feat: added timer controls and timer examples (#11)
* feat: added timeout shared value wip * feat: timeout pagination and example * feat: added timer to param * feat: added variant interval * chore: refactored carousel logic into hooks * chore: fixed linter issues * chore: added ability to run autoscroll from slides * feat: added video example * fix: added player pausing wip * fix: added video pausing * fix: android crash * fix: renamed timer pagination * fix: android issues --------- Co-authored-by: Petr Konecny <[email protected]>
1 parent 819c89b commit 6c04a6a

File tree

18 files changed

+834
-96
lines changed

18 files changed

+834
-96
lines changed

example/app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"resizeMode": "contain",
3737
"backgroundColor": "#ffffff"
3838
}
39-
]
39+
],
40+
"expo-video"
4041
],
4142
"experiments": {
4243
"typedRoutes": true

example/app/(examples)/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const examples = [
2424
description: 'Carousel with entering/exiting animations triggered by shared values',
2525
route: '/entering-animation' as const,
2626
},
27+
{
28+
title: 'Timer Pagination Carousel',
29+
description: 'Timer-based pagination with auto-slide progress indicator',
30+
route: '/timer-pagination' as const,
31+
},
32+
{
33+
title: 'Video Carousel',
34+
description: 'A carousel showcasing videos using expo-video',
35+
route: '/video-carousel' as const,
36+
},
2737
]
2838

2939
export default function HomeScreen() {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import TimerPaginationExample from '@/examples/TimerPaginationExample'
2+
3+
export default TimerPaginationExample
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import VideoCarouselExample from '@/examples/VideoCarouselExample'
2+
3+
export default VideoCarouselExample

example/examples/EnteringAnimationExample.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n
3333

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

7070
const styles = StyleSheet.create({
71+
textContainer: {
72+
flex: 1,
73+
},
7174
container: {
7275
flex: 1,
7376
backgroundColor: '#fff',
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import {
2+
AutoCarousel,
3+
CarouselContextProvider,
4+
useAutoCarouselSlideIndex,
5+
} from '@strv/react-native-hero-carousel'
6+
import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native'
7+
import { Image } from 'expo-image'
8+
import { LinearGradient } from 'expo-linear-gradient'
9+
import { useEffect } from 'react'
10+
import { BlurView } from 'expo-blur'
11+
import { TimerPagination } from './components/TimerPagination'
12+
13+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
14+
15+
const getRandomImageUrl = () => {
16+
return `https://picsum.photos/${SCREEN_WIDTH}/${SCREEN_HEIGHT}?random=${Math.floor(Math.random() * 1000)}`
17+
}
18+
19+
const images = Array.from({ length: 5 }, getRandomImageUrl)
20+
21+
const Slide = ({
22+
image,
23+
title,
24+
getInterval,
25+
}: {
26+
image: string
27+
title: string
28+
index: number
29+
getInterval: (index: number) => number
30+
}) => {
31+
const { index: currentIndex } = useAutoCarouselSlideIndex()
32+
const interval = getInterval(currentIndex)
33+
34+
return (
35+
<View style={styles.slide}>
36+
<Image key={image} source={{ uri: image }} style={styles.image} contentFit="cover" />
37+
<LinearGradient colors={['rgba(0,0,0,0.8)', 'transparent']} style={styles.topGradient} />
38+
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.8)']} style={styles.gradient}>
39+
<BlurView style={styles.blurView}>
40+
<Text style={styles.title}>{title}</Text>
41+
<Text style={styles.subtitle}>Slide change interval: {interval / 1000} s</Text>
42+
</BlurView>
43+
</LinearGradient>
44+
</View>
45+
)
46+
}
47+
48+
export default function TimerPaginationExample() {
49+
// Preload all images when component mounts
50+
useEffect(() => {
51+
Image.prefetch(images)
52+
}, [])
53+
54+
const getInterval = (index: number) => {
55+
'worklet'
56+
return index * 3000
57+
}
58+
59+
return (
60+
<CarouselContextProvider>
61+
<SafeAreaView style={styles.container}>
62+
<View style={styles.container}>
63+
<AutoCarousel interval={getInterval}>
64+
{images.map((image, index) => (
65+
<Slide
66+
key={index}
67+
image={image}
68+
title={`Slide ${index + 1}`}
69+
index={index}
70+
getInterval={getInterval}
71+
/>
72+
))}
73+
</AutoCarousel>
74+
<TimerPagination total={images.length} hideProgressOnInteraction />
75+
</View>
76+
</SafeAreaView>
77+
</CarouselContextProvider>
78+
)
79+
}
80+
81+
const styles = StyleSheet.create({
82+
container: {
83+
flex: 1,
84+
backgroundColor: '#000',
85+
},
86+
slide: {
87+
flex: 1,
88+
width: '100%',
89+
height: '100%',
90+
overflow: 'hidden',
91+
},
92+
image: {
93+
width: '100%',
94+
height: '100%',
95+
transformOrigin: 'center',
96+
transform: [{ scale: 1.6 }],
97+
},
98+
gradient: {
99+
position: 'absolute',
100+
bottom: 0,
101+
left: 0,
102+
right: 0,
103+
height: '50%',
104+
justifyContent: 'flex-end',
105+
padding: 20,
106+
},
107+
topGradient: {
108+
position: 'absolute',
109+
top: 0,
110+
left: 0,
111+
right: 0,
112+
height: '20%',
113+
},
114+
title: {
115+
fontSize: 32,
116+
lineHeight: 32,
117+
fontWeight: 'bold',
118+
color: 'white',
119+
},
120+
subtitle: {
121+
fontSize: 16,
122+
lineHeight: 16,
123+
fontWeight: '500',
124+
color: 'white',
125+
opacity: 0.8,
126+
},
127+
paginationContainer: {
128+
position: 'absolute',
129+
top: 20,
130+
left: 20,
131+
right: 20,
132+
overflow: 'hidden',
133+
flexDirection: 'row',
134+
gap: 8,
135+
borderRadius: 16,
136+
borderWidth: 1,
137+
borderColor: 'rgba(255, 255, 255, 0.05)',
138+
zIndex: 10,
139+
},
140+
paginationDot: {
141+
flex: 1,
142+
height: 3,
143+
backgroundColor: 'rgba(255, 255, 255, 0.3)',
144+
borderRadius: 2,
145+
overflow: 'hidden',
146+
},
147+
dotBackground: {
148+
position: 'absolute',
149+
top: 0,
150+
left: 0,
151+
right: 0,
152+
bottom: 0,
153+
backgroundColor: 'rgba(255, 255, 255, 0.5)',
154+
},
155+
dotProgress: {
156+
position: 'absolute',
157+
top: 0,
158+
left: 0,
159+
bottom: 0,
160+
backgroundColor: 'white',
161+
borderRadius: 2,
162+
},
163+
blurView: {
164+
position: 'absolute',
165+
bottom: 20,
166+
padding: 20,
167+
margin: 8,
168+
borderRadius: 16,
169+
gap: 8,
170+
borderWidth: 1,
171+
borderColor: 'rgba(255, 255, 255, 0.05)',
172+
overflow: 'hidden',
173+
},
174+
})
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {
2+
AutoCarousel,
3+
CarouselContextProvider,
4+
useAutoCarouselSlideIndex,
5+
} from '@strv/react-native-hero-carousel'
6+
import { SafeAreaView, StyleSheet, View, Text, Pressable, Dimensions, Platform } from 'react-native'
7+
import { useVideoPlayer, VideoView } from 'expo-video'
8+
import { LinearGradient } from 'expo-linear-gradient'
9+
import { useActiveSlideEffect, useIsActiveSlide } from '@/hooks/useActiveSlideEffect'
10+
import { useEffect, useRef, useState } from 'react'
11+
import { TimerPagination } from './components/TimerPagination'
12+
import { useEvent, useEventListener } from 'expo'
13+
14+
const { width, height } = Dimensions.get('window')
15+
// Sample video URLs - these are publicly available videos that work well for testing
16+
const videos = [
17+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
18+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
19+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4',
20+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
21+
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
22+
]
23+
24+
const videoTitles = [
25+
'For Bigger Blazes',
26+
'For Bigger Escapes',
27+
'For Bigger Fun',
28+
'Big Buck Bunny',
29+
'Elephants Dream',
30+
]
31+
32+
const Slide = ({ videoUri, title, index }: { videoUri: string; title: string; index: number }) => {
33+
const player = useVideoPlayer(videoUri)
34+
const { runAutoScroll } = useAutoCarouselSlideIndex()
35+
const isActiveSlide = useIsActiveSlide()
36+
const [duration, setDuration] = useState(0)
37+
useActiveSlideEffect(() => {
38+
player.currentTime = 0
39+
player.play()
40+
return () => {
41+
player.pause()
42+
}
43+
})
44+
45+
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing })
46+
47+
useEventListener(player, 'statusChange', ({ status }) => {
48+
if (status === 'readyToPlay') {
49+
setDuration(player.duration)
50+
}
51+
})
52+
53+
const intervalRef = useRef<ReturnType<typeof runAutoScroll> | null>(null)
54+
55+
useEffect(() => {
56+
if (isActiveSlide && duration) {
57+
intervalRef.current = runAutoScroll(duration * 1000)
58+
}
59+
}, [isActiveSlide, duration, runAutoScroll])
60+
61+
return (
62+
<View style={styles.slide}>
63+
<Pressable
64+
key={index}
65+
style={styles.slide}
66+
onPress={() => {
67+
if (isPlaying) {
68+
player.pause()
69+
intervalRef.current?.pause()
70+
} else {
71+
player.play()
72+
intervalRef.current?.resume()
73+
}
74+
}}
75+
>
76+
<VideoView
77+
player={player}
78+
style={styles.video}
79+
contentFit={Platform.OS === 'android' ? 'fill' : 'cover'}
80+
nativeControls={false}
81+
/>
82+
<LinearGradient colors={['rgba(0,0,0,0.8)', 'transparent']} style={styles.topGradient} />
83+
<LinearGradient colors={['transparent', 'rgba(0,0,0,0.8)']} style={styles.gradient}>
84+
<Text style={styles.title}>{title}</Text>
85+
<Text style={styles.subtitle}>Swipe to navigate • Tap to play/pause</Text>
86+
</LinearGradient>
87+
</Pressable>
88+
</View>
89+
)
90+
}
91+
92+
export default function VideoCarouselExample() {
93+
return (
94+
<CarouselContextProvider>
95+
<SafeAreaView style={styles.container}>
96+
<View style={styles.container}>
97+
<AutoCarousel disableAutoScroll={true}>
98+
{videos.map((video, index) => (
99+
<Slide key={index} videoUri={video} title={videoTitles[index]} index={index} />
100+
))}
101+
</AutoCarousel>
102+
<TimerPagination total={videos.length} hideProgressOnInteraction={false} />
103+
</View>
104+
</SafeAreaView>
105+
</CarouselContextProvider>
106+
)
107+
}
108+
109+
const styles = StyleSheet.create({
110+
container: {
111+
flex: 1,
112+
backgroundColor: '#000',
113+
},
114+
slide: {
115+
flex: 1,
116+
width: '100%',
117+
height: '100%',
118+
overflow: 'hidden',
119+
},
120+
video: {
121+
width: width,
122+
height: height,
123+
},
124+
gradient: {
125+
position: 'absolute',
126+
bottom: 0,
127+
left: 0,
128+
right: 0,
129+
height: '50%',
130+
justifyContent: 'flex-end',
131+
padding: 20,
132+
},
133+
topGradient: {
134+
position: 'absolute',
135+
top: 0,
136+
left: 0,
137+
right: 0,
138+
height: '20%',
139+
},
140+
title: {
141+
fontSize: 32,
142+
bottom: 60,
143+
left: 20,
144+
position: 'absolute',
145+
lineHeight: 32,
146+
fontWeight: 'bold',
147+
color: 'white',
148+
},
149+
subtitle: {
150+
fontSize: 16,
151+
bottom: 20,
152+
left: 20,
153+
position: 'absolute',
154+
lineHeight: 20,
155+
color: 'white',
156+
opacity: 0.8,
157+
},
158+
})

0 commit comments

Comments
 (0)