import React, { useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; import { View, Animated, Text, Platform, Easing, StyleSheet } from 'react-native'; import type { StyleProp, TextStyle, LayoutChangeEvent } from 'react-native'; import type { NoticeBarProps, NoticeBarInstance } from './interface'; type MarqueeProps = Pick< NoticeBarProps, 'wrapable' | 'scrollable' | 'speed' | 'delay' | 'text' | 'onReplay' > & { style: StyleProp; }; const isWeb = Platform.OS === 'web'; const Marquee = forwardRef((props, ref) => { const { scrollable = false, speed = 60, delay = 1000, style, text, wrapable, onReplay } = props; // 内容的 translateX 值 const translateX = useRef(new Animated.Value(0)).current; // 内容容器的宽度 const wrapWidth = useRef(0); // 内容实际的宽度 const contentWidth = useRef(0); const startTimer = useRef(null); const translateAnimated = useRef(); const handleTextLayout = (event: LayoutChangeEvent) => { contentWidth.current = event.nativeEvent.layout.width; }; const handleContainerLayout = (event: LayoutChangeEvent) => { wrapWidth.current = event.nativeEvent.layout.width; }; // 开始尝试开始动画 const tryStart = () => { /** * 开始移动需要满足几个条件 * 1. 文本的长度要大于容器的宽度 * 2. 不允许换行 * 3. 允许滚动 */ if (!wrapable && scrollable && contentWidth.current > wrapWidth.current) { startMove(); } }; // 开始滚动 const startMove = () => { translateAnimated.current = Animated.loop( Animated.sequence([ Animated.timing(translateX, { toValue: -contentWidth.current, duration: (contentWidth.current / speed) * 1000, easing: Easing.linear, useNativeDriver: !isWeb, }), Animated.timing(translateX, { toValue: wrapWidth.current, duration: 0, easing: Easing.linear, useNativeDriver: !isWeb, }), Animated.timing(translateX, { toValue: 0, duration: (contentWidth.current / speed) * 1000, easing: Easing.linear, useNativeDriver: !isWeb, }), ]) ); translateAnimated.current.start(() => { onReplay?.(); }); }; const reset = () => { startTimer.current && clearTimeout(startTimer.current); translateAnimated.current && translateAnimated.current.reset(); startTimer.current = setTimeout(() => { tryStart(); }, delay ?? 0); }; useEffect(() => { reset(); }, [text, scrollable, wrapable]); useImperativeHandle(ref, () => ({ reset })); return ( {/* TODO: 必须让 View 绝对定位,才能用 onLayout 获取到文本的宽度 */} {wrapable ? ( {text} ) : ( {text} )} ); }); const styles = StyleSheet.create({ container: { alignItems: 'center', flex: 1, flexDirection: 'row', position: 'relative', }, }); export default Marquee;