|
|
@@ -0,0 +1,175 @@
|
|
|
+import { Image } from 'expo-image';
|
|
|
+import React, { useEffect, useRef, useState } from 'react';
|
|
|
+import { Animated, Easing, ImageBackground, StyleSheet, Text, View } from 'react-native';
|
|
|
+
|
|
|
+import { Images } from '@/constants/images';
|
|
|
+
|
|
|
+interface BarrageItemType {
|
|
|
+ id?: string;
|
|
|
+ poolId?: string;
|
|
|
+ nickname?: string;
|
|
|
+ poolName?: string;
|
|
|
+ text?: string; // amount or feedback
|
|
|
+ type?: string; // '奖池'
|
|
|
+ avatar?: string;
|
|
|
+}
|
|
|
+
|
|
|
+interface BarrageProps {
|
|
|
+ data: BarrageItemType[];
|
|
|
+ speed?: number; // ms per pixel? Or just a factor.
|
|
|
+ style?: any;
|
|
|
+}
|
|
|
+
|
|
|
+export const Barrage: React.FC<BarrageProps> = ({ data, speed = 30, style }) => {
|
|
|
+ const [contentWidth, setContentWidth] = useState(0);
|
|
|
+ const translateX = useRef(new Animated.Value(0)).current;
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (contentWidth > 0 && data.length > 0) {
|
|
|
+ startAnimation();
|
|
|
+ }
|
|
|
+ }, [contentWidth, data]);
|
|
|
+
|
|
|
+ const startAnimation = () => {
|
|
|
+ // Reset to 0
|
|
|
+ translateX.setValue(0);
|
|
|
+
|
|
|
+ // Duration: width * factor.
|
|
|
+ // If width is 1000, speed 30 -> 30000ms (30s).
|
|
|
+ const duration = contentWidth * speed;
|
|
|
+
|
|
|
+ Animated.loop(
|
|
|
+ Animated.timing(translateX, {
|
|
|
+ toValue: -contentWidth,
|
|
|
+ duration: duration,
|
|
|
+ easing: Easing.linear,
|
|
|
+ useNativeDriver: true,
|
|
|
+ })
|
|
|
+ ).start();
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderItem = (item: BarrageItemType, index: number) => {
|
|
|
+ const isPool = item.poolId && Number(item.text || '0') > 0;
|
|
|
+ // item.type == '奖池' logic from Vue
|
|
|
+
|
|
|
+ return (
|
|
|
+ <ImageBackground
|
|
|
+ key={`${item.id || index}-main`}
|
|
|
+ source={{ uri: Images.box.barrageItem }}
|
|
|
+ style={styles.itemBg}
|
|
|
+ resizeMode="stretch"
|
|
|
+ >
|
|
|
+ <View style={styles.contentRow}>
|
|
|
+ {/* Avatar */}
|
|
|
+ <View style={styles.avatarBox}>
|
|
|
+ <Image source={{ uri: item.avatar }} style={styles.avatar} contentFit="cover" />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* Text Content */}
|
|
|
+ <View style={styles.textContainer}>
|
|
|
+ {isPool ? (
|
|
|
+ <Text style={styles.text} numberOfLines={1}>
|
|
|
+ <Text style={styles.nickname}>{item.nickname?.slice(0,1) + '***' + item.nickname?.slice(-1)}</Text>
|
|
|
+ <Text> 在 </Text>
|
|
|
+ <Text style={styles.poolName}>{item.poolName}</Text>
|
|
|
+ <Text> {item.type === '奖池' ? '消费了' : '获得'} </Text>
|
|
|
+ <Text style={styles.amount}>{item.text}</Text>
|
|
|
+ <Text> {item.type === '奖池' ? '元' : ''} </Text>
|
|
|
+ </Text>
|
|
|
+ ) : (
|
|
|
+ <Text style={styles.text} numberOfLines={1}>
|
|
|
+ <Text style={styles.nickname}>{item.nickname?.slice(0,1) + '***' + item.nickname?.slice(-1)}</Text>
|
|
|
+ <Text>: {item.text}</Text>
|
|
|
+ </Text>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </ImageBackground>
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ if (!data || data.length === 0) return null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View style={[styles.container, style]}>
|
|
|
+ <Animated.View
|
|
|
+ style={[
|
|
|
+ styles.scrollContainer,
|
|
|
+ { transform: [{ translateX }] },
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ {/* Measure the width of the first set */}
|
|
|
+ <View
|
|
|
+ style={styles.row}
|
|
|
+ onLayout={(e) => setContentWidth(e.nativeEvent.layout.width)}
|
|
|
+ >
|
|
|
+ {data.map((item, index) => renderItem(item, index))}
|
|
|
+ </View>
|
|
|
+ {/* Duplicate set for seamless loop */}
|
|
|
+ <View style={styles.row}>
|
|
|
+ {data.map((item, index) => renderItem(item, index))}
|
|
|
+ </View>
|
|
|
+ </Animated.View>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ container: {
|
|
|
+ width: '100%',
|
|
|
+ overflow: 'hidden',
|
|
|
+ height: 40, // Adjust based on item height
|
|
|
+ },
|
|
|
+ scrollContainer: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ },
|
|
|
+ row: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ },
|
|
|
+ itemBg: {
|
|
|
+ paddingHorizontal: 12, // Reduced padding
|
|
|
+ paddingVertical: 4,
|
|
|
+ marginRight: 10,
|
|
|
+ justifyContent: 'center',
|
|
|
+ height: 32, // Fixed height
|
|
|
+ minWidth: 150,
|
|
|
+ },
|
|
|
+ contentRow: {
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ },
|
|
|
+ avatarBox: {
|
|
|
+ marginRight: 6,
|
|
|
+ width: 24,
|
|
|
+ height: 24,
|
|
|
+ borderRadius: 12,
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: '#000',
|
|
|
+ overflow: 'hidden',
|
|
|
+ backgroundColor: '#fff',
|
|
|
+ },
|
|
|
+ avatar: {
|
|
|
+ width: '100%',
|
|
|
+ height: '100%',
|
|
|
+ },
|
|
|
+ textContainer: {
|
|
|
+ justifyContent: 'center',
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ color: '#fff',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ nickname: {
|
|
|
+ fontWeight: 'bold',
|
|
|
+ },
|
|
|
+ poolName: {
|
|
|
+ color: '#0084FF',
|
|
|
+ fontSize: 10,
|
|
|
+ },
|
|
|
+ amount: {
|
|
|
+ color: '#FF0000',
|
|
|
+ fontSize: 11,
|
|
|
+ fontWeight: 'bold',
|
|
|
+ }
|
|
|
+});
|