Barrage.tsx 4.7 KB

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