Barrage.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import { Image } from 'expo-image';
  2. import React, { useEffect, useRef, useState } from 'react';
  3. import { Animated, Easing, ImageBackground, StyleSheet, Text, View } from 'react-native';
  4. import { Images } from '@/constants/images';
  5. interface BarrageItemType {
  6. id?: string;
  7. poolId?: string;
  8. nickname?: string;
  9. poolName?: string;
  10. text?: string; // amount or feedback
  11. type?: string; // '奖池'
  12. avatar?: string;
  13. }
  14. interface BarrageProps {
  15. data: BarrageItemType[];
  16. speed?: number; // ms per pixel? Or just a factor.
  17. style?: any;
  18. }
  19. export const Barrage: React.FC<BarrageProps> = ({ data, speed = 30, style }) => {
  20. const [contentWidth, setContentWidth] = useState(0);
  21. const translateX = useRef(new Animated.Value(0)).current;
  22. useEffect(() => {
  23. if (contentWidth > 0 && data.length > 0) {
  24. startAnimation();
  25. }
  26. }, [contentWidth, data]);
  27. const startAnimation = () => {
  28. // Reset to 0
  29. translateX.setValue(0);
  30. // Duration: width * factor.
  31. // If width is 1000, speed 30 -> 30000ms (30s).
  32. const duration = contentWidth * speed;
  33. Animated.loop(
  34. Animated.timing(translateX, {
  35. toValue: -contentWidth,
  36. duration: duration,
  37. easing: Easing.linear,
  38. useNativeDriver: true,
  39. })
  40. ).start();
  41. };
  42. const renderItem = (item: BarrageItemType, index: number) => {
  43. const isPool = item.poolId && Number(item.text || '0') > 0;
  44. // item.type == '奖池' logic from Vue
  45. return (
  46. <ImageBackground
  47. key={`${item.id || index}-main`}
  48. source={{ uri: Images.box.barrageItem }}
  49. style={styles.itemBg}
  50. resizeMode="stretch"
  51. >
  52. <View style={styles.contentRow}>
  53. {/* Avatar */}
  54. <View style={styles.avatarBox}>
  55. <Image source={{ uri: item.avatar }} style={styles.avatar} contentFit="cover" />
  56. </View>
  57. {/* Text Content */}
  58. <View style={styles.textContainer}>
  59. {isPool ? (
  60. <Text style={styles.text} numberOfLines={1}>
  61. <Text style={styles.nickname}>{item.nickname?.slice(0,1) + '***' + item.nickname?.slice(-1)}</Text>
  62. <Text> 在 </Text>
  63. <Text style={styles.poolName}>{item.poolName}</Text>
  64. <Text> {item.type === '奖池' ? '消费了' : '获得'} </Text>
  65. <Text style={styles.amount}>{item.text}</Text>
  66. <Text> {item.type === '奖池' ? '元' : ''} </Text>
  67. </Text>
  68. ) : (
  69. <Text style={styles.text} numberOfLines={1}>
  70. <Text style={styles.nickname}>{item.nickname?.slice(0,1) + '***' + item.nickname?.slice(-1)}</Text>
  71. <Text>: {item.text}</Text>
  72. </Text>
  73. )}
  74. </View>
  75. </View>
  76. </ImageBackground>
  77. );
  78. };
  79. if (!data || data.length === 0) return null;
  80. return (
  81. <View style={[styles.container, style]}>
  82. <Animated.View
  83. style={[
  84. styles.scrollContainer,
  85. { transform: [{ translateX }] },
  86. ]}
  87. >
  88. {/* Measure the width of the first set */}
  89. <View
  90. style={styles.row}
  91. onLayout={(e) => setContentWidth(e.nativeEvent.layout.width)}
  92. >
  93. {data.map((item, index) => renderItem(item, index))}
  94. </View>
  95. {/* Duplicate set for seamless loop */}
  96. <View style={styles.row}>
  97. {data.map((item, index) => renderItem(item, index))}
  98. </View>
  99. </Animated.View>
  100. </View>
  101. );
  102. };
  103. const styles = StyleSheet.create({
  104. container: {
  105. width: '100%',
  106. overflow: 'hidden',
  107. height: 40, // Adjust based on item height
  108. },
  109. scrollContainer: {
  110. flexDirection: 'row',
  111. },
  112. row: {
  113. flexDirection: 'row',
  114. alignItems: 'center',
  115. },
  116. itemBg: {
  117. paddingHorizontal: 12, // Reduced padding
  118. paddingVertical: 4,
  119. marginRight: 10,
  120. justifyContent: 'center',
  121. height: 32, // Fixed height
  122. minWidth: 150,
  123. },
  124. contentRow: {
  125. flexDirection: 'row',
  126. alignItems: 'center',
  127. },
  128. avatarBox: {
  129. marginRight: 6,
  130. width: 24,
  131. height: 24,
  132. borderRadius: 12,
  133. borderWidth: 1,
  134. borderColor: '#000',
  135. overflow: 'hidden',
  136. backgroundColor: '#fff',
  137. },
  138. avatar: {
  139. width: '100%',
  140. height: '100%',
  141. },
  142. textContainer: {
  143. justifyContent: 'center',
  144. },
  145. text: {
  146. color: '#fff',
  147. fontSize: 10,
  148. },
  149. nickname: {
  150. fontWeight: 'bold',
  151. },
  152. poolName: {
  153. color: '#0084FF',
  154. fontSize: 10,
  155. },
  156. amount: {
  157. color: '#FF0000',
  158. fontSize: 11,
  159. fontWeight: 'bold',
  160. }
  161. });