diff --git a/example/app/(examples)/entering-animation.tsx b/example/app/(examples)/entering-animation.tsx new file mode 100644 index 0000000..64690ec --- /dev/null +++ b/example/app/(examples)/entering-animation.tsx @@ -0,0 +1,3 @@ +import EnteringAnimationExample from '@/examples/EnteringAnimationExample' + +export default EnteringAnimationExample diff --git a/example/app/(examples)/index.tsx b/example/app/(examples)/index.tsx index ad0533f..996184b 100644 --- a/example/app/(examples)/index.tsx +++ b/example/app/(examples)/index.tsx @@ -19,6 +19,11 @@ const examples = [ description: 'A carousel with offset animations', route: '/offset' as const, }, + { + title: 'Entering Animation Carousel', + description: 'Carousel with entering/exiting animations triggered by shared values', + route: '/entering-animation' as const, + }, ] export default function HomeScreen() { diff --git a/example/examples/AnimatedExample.tsx b/example/examples/AnimatedExample.tsx index 2cf4871..6404ab3 100644 --- a/example/examples/AnimatedExample.tsx +++ b/example/examples/AnimatedExample.tsx @@ -10,6 +10,7 @@ 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' +import { useEffect } from 'react' const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window') @@ -63,6 +64,11 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n } export default function AnimatedExample() { + // Preload all images when component mounts + useEffect(() => { + Image.prefetch(images) + }, []) + return ( diff --git a/example/examples/BasicExample.tsx b/example/examples/BasicExample.tsx index e74da3e..3e01459 100644 --- a/example/examples/BasicExample.tsx +++ b/example/examples/BasicExample.tsx @@ -2,6 +2,7 @@ import { AutoCarousel, CarouselContextProvider } from '@strv/react-native-hero-c import { SafeAreaView, StyleSheet, View, Text, Dimensions } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' +import { useEffect } from 'react' const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window') @@ -14,7 +15,7 @@ const images = Array.from({ length: 5 }, getRandomImageUrl) const Slide = ({ image, title, index }: { image: string; title: string; index: number }) => { return ( - + {title} @@ -23,6 +24,11 @@ const Slide = ({ image, title, index }: { image: string; title: string; index: n } export default function BasicExample() { + // Preload all images when component mounts + useEffect(() => { + Image.prefetch(images) + }, []) + return ( diff --git a/example/examples/EnteringAnimationExample.tsx b/example/examples/EnteringAnimationExample.tsx new file mode 100644 index 0000000..22729fd --- /dev/null +++ b/example/examples/EnteringAnimationExample.tsx @@ -0,0 +1,116 @@ +import { + AutoCarousel, + CarouselContextProvider, + SlideAnimatedView, +} 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 { FadeIn, SlideInDown, SlideInRight, ZoomIn, FlipInEasyX } from 'react-native-reanimated' +import { useEffect } from 'react' + +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 animationNames = ['FadeIn', 'SlideInDown', 'SlideInRight', 'ZoomIn', 'FlipInEasyX'] + +const Slide = ({ image, title, index }: { image: string; title: string; index: number }) => { + // Different animation types for each slide to showcase variety + const animationConfigs = [ + { entering: FadeIn.duration(400) }, + { entering: SlideInDown.duration(500) }, + { entering: SlideInRight.duration(600) }, + { entering: ZoomIn.duration(700) }, + { entering: FlipInEasyX.duration(800) }, + ] + + const animationConfig = animationConfigs[index % animationConfigs.length] + + return ( + + + + + {title} + + Animation: {animationNames[index % animationNames.length]} + + + + + ) +} + +export default function EnteringAnimationExample() { + // Preload all images when component mounts + useEffect(() => { + Image.prefetch(images) + }, []) + + return ( + + + + + {images.map((image, index) => ( + + ))} + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + slide: { + flex: 1, + width: '100%', + height: '100%', + overflow: 'hidden', + backgroundColor: 'black', + }, + 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', + }, + subtitle: { + fontSize: 16, + bottom: 70, + left: 20, + position: 'absolute', + lineHeight: 16, + fontWeight: '500', + color: 'white', + opacity: 0.8, + }, +}) diff --git a/example/examples/OffsetExample.tsx b/example/examples/OffsetExample.tsx index ae318a4..d7ea556 100644 --- a/example/examples/OffsetExample.tsx +++ b/example/examples/OffsetExample.tsx @@ -9,6 +9,7 @@ import { SafeAreaView, StyleSheet, View, Dimensions } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' import Animated, { useAnimatedStyle } from 'react-native-reanimated' +import { useEffect } from 'react' const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window') @@ -89,7 +90,7 @@ const Slide = ({ return ( - + {title} @@ -106,6 +107,11 @@ const Slide = ({ } export default function OffsetExample() { + // Preload all images when component mounts + useEffect(() => { + Image.prefetch(images) + }, []) + return ( diff --git a/src/components/SlideAnimatedView/index.tsx b/src/components/SlideAnimatedView/index.tsx new file mode 100644 index 0000000..5e2f49d --- /dev/null +++ b/src/components/SlideAnimatedView/index.tsx @@ -0,0 +1,71 @@ +import { useAutoCarouselSlideIndex, useCarouselContext } from '../../context' +import Animated, { + useDerivedValue, + useAnimatedReaction, + runOnJS, + AnimatedProps, +} from 'react-native-reanimated' +import { interpolateInsideCarousel } from '../../utils' +import { useState } from 'react' +import { ViewProps } from 'react-native' + +type SlideAnimatedViewProps = { + children: React.ReactNode + entering?: AnimatedProps['entering'] + exiting?: AnimatedProps['exiting'] + layout?: AnimatedProps['layout'] + enteringThreshold?: number + exitingThreshold?: number +} + +export const SlideAnimatedView = ({ + children, + entering, + exiting, + layout, + enteringThreshold = 0.99, + exitingThreshold = 0.01, +}: SlideAnimatedViewProps) => { + const { index, total } = useAutoCarouselSlideIndex() + const { scrollValue } = useCarouselContext() + const [shouldShow, setShouldShow] = useState(false) + + const value = useDerivedValue(() => { + return interpolateInsideCarousel(scrollValue.value, index, total, { + valueBefore: 0, + thisValue: 1, + valueAfter: 0, + }) + }) + + // Track when value becomes 1 to trigger entering animation + useAnimatedReaction( + () => value.value, + (currentValue, previousValue) => { + // Trigger entering animation when value becomes 1 + if ( + currentValue >= enteringThreshold && + (previousValue === null || previousValue < enteringThreshold) + ) { + runOnJS(setShouldShow)(true) + } + // Trigger exiting animation when value goes back to 0 + if ( + currentValue <= exitingThreshold && + (previousValue === null || previousValue > exitingThreshold) + ) { + runOnJS(setShouldShow)(false) + } + }, + ) + + if (!shouldShow) { + return null + } + + return ( + + {children} + + ) +} diff --git a/src/components/index.ts b/src/components/index.ts index f111284..5ee9516 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,3 +2,4 @@ export * from './Carousel' export * from './AnimatedPagedScrollView' export * from './AutoCarousel' export * from './AutoCarouselSlide' +export * from './SlideAnimatedView'