File size: 3,768 Bytes
9a92a42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet, Image, Animated, Easing } from 'react-native';
import MapView, { Marker } from 'react-native-maps';
import { Ionicons } from '@expo/vector-icons';
const AnimatedMapPin = ({ training }) => {
const [showPopup, setShowPopup] = useState(false);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const scaleAnim = useRef(new Animated.Value(0)).current; // For popup appear/disappear
const opacityAnim = useRef(new Animated.Value(0)).current; // For image fade in/out
const hasImages = training.photos && training.photos.length > 0;
useEffect(() => {
if (!hasImages) return;
const mainLoop = () => {
// 1. Appear
setShowPopup(true);
Animated.spring(scaleAnim, {
toValue: 1,
friction: 5,
useNativeDriver: true,
}).start(() => {
// 2. Start image slideshow
runSlideshow();
});
};
const runSlideshow = () => {
// Fade in the first image
fadeImage(true, () => {
// After fade-in, wait, then start cycling
setTimeout(() => cycleImages(1), 2000);
});
};
const cycleImages = (nextIndex) => {
// Fade out current image
fadeImage(false, () => {
if (nextIndex >= training.photos.length) {
// Slideshow finished, hide the popup
hidePopup();
} else {
// Show next image
setCurrentImageIndex(nextIndex);
fadeImage(true, () => {
// Wait, then cycle to the next one
setTimeout(() => cycleImages(nextIndex + 1), 2000);
});
}
});
};
const fadeImage = (fadeIn, onComplete) => {
Animated.timing(opacityAnim, {
toValue: fadeIn ? 1 : 0,
duration: 800, // Smooth fade duration
easing: Easing.ease,
useNativeDriver: true,
}).start(onComplete);
};
const hidePopup = () => {
Animated.timing(scaleAnim, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setShowPopup(false);
setCurrentImageIndex(0); // Reset for next loop
// 3. Disappear for 1 second, then restart loop
setTimeout(mainLoop, 1000);
});
};
// Start the main loop
mainLoop();
}, [training, hasImages]);
if (!training.location) return null;
return (
<Marker
coordinate={{
latitude: training.location.latitude,
longitude: training.location.longitude,
}}
anchor={{ x: 0.5, y: 0.5 }}
>
<View style={styles.markerContainer}>
{showPopup && hasImages && (
<Animated.View style={[styles.popup, { transform: [{ scale: scaleAnim }] }]}>
<Animated.Image
source={{ uri: training.photos[currentImageIndex] }}
style={[styles.popupImage, { opacity: opacityAnim }]}
resizeMode="cover"
/>
</Animated.View>
)}
<View style={styles.pin}>
<Ionicons name="location" size={40} color="#FF5A5F" />
</View>
</View>
</Marker>
);
};
const styles = StyleSheet.create({
markerContainer: {
alignItems: 'center',
justifyContent: 'center',
},
pin: {
// The pin itself
},
popup: {
position: 'absolute',
bottom: 35, // Position above the pin
width: 80,
height: 80,
backgroundColor: 'white',
borderRadius: 10,
padding: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
elevation: 5,
},
popupImage: {
width: '100%',
height: '100%',
borderRadius: 6,
},
});
export default AnimatedMapPin;
|