import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Animated, Easing, LayoutChangeEvent, StyleProp, StyleSheet, Text, TextProps, TextStyle, View, ViewStyle, } from 'react-native'; interface MarqueeTextProps extends Omit { text: string; enabled?: boolean; style?: StyleProp; containerStyle?: StyleProp; gap?: number; speed?: number; initialDelayMs?: number; } export const MarqueeText: React.FC = ({ text, enabled = false, style, containerStyle, gap = 40, speed = 28, initialDelayMs = 1200, ...textProps }) => { const translateX = useRef(new Animated.Value(0)).current; const animationRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const [textWidth, setTextWidth] = useState(0); const normalizedText = useMemo(() => String(text || ''), [text]); const shouldAnimate = enabled && containerWidth > 0 && textWidth > containerWidth + 4; const distance = Math.max(0, textWidth + gap); const durationMs = Math.max(6000, Math.round((distance / Math.max(speed, 1)) * 1000)); useEffect(() => { animationRef.current?.stop(); translateX.stopAnimation(); translateX.setValue(0); if (!shouldAnimate || !distance) { return () => { animationRef.current?.stop(); }; } const sequence = Animated.loop( Animated.sequence([ Animated.delay(initialDelayMs), Animated.timing(translateX, { toValue: -distance, duration: durationMs, easing: Easing.linear, useNativeDriver: true, }), Animated.delay(700), Animated.timing(translateX, { toValue: 0, duration: 0, useNativeDriver: true, }), ]) ); animationRef.current = sequence; sequence.start(); return () => { sequence.stop(); animationRef.current = null; translateX.stopAnimation(); translateX.setValue(0); }; }, [distance, durationMs, initialDelayMs, shouldAnimate, translateX]); const handleContainerLayout = (event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width || 0); if (nextWidth !== containerWidth) { setContainerWidth(nextWidth); } }; const handleTextLayout = (event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width || 0); if (nextWidth !== textWidth) { setTextWidth(nextWidth); } }; return ( {normalizedText} {shouldAnimate ? ( {normalizedText} {normalizedText} ) : ( {normalizedText} )} ); }; const styles = StyleSheet.create({ container: { width: '100%', overflow: 'hidden', }, track: { flexDirection: 'row', alignItems: 'center', }, measureText: { position: 'absolute', opacity: 0, left: 0, top: -9999, }, });