LotteryGrid.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import { Image } from 'expo-image';
  2. import React, { useEffect, useRef, useState } from 'react';
  3. import { Animated, Dimensions, ScrollView, StyleSheet, View } from 'react-native';
  4. import { Images } from '@/constants/images';
  5. import TransCard from './TransCard';
  6. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  7. // 3 columns. Uniapp: 220rpx (approx 30%).
  8. // Let's use 3 columns with some spacing.
  9. const COLUMNS = 3;
  10. const ITEM_WIDTH = (SCREEN_WIDTH - 40) / COLUMNS; // 20px padding left/right
  11. const ITEM_HEIGHT = ITEM_WIDTH * 1.5; // Aspect ratio
  12. interface FlipCardProps {
  13. item: any;
  14. index: number;
  15. delay: number;
  16. onFlipComplete?: () => void;
  17. }
  18. const FlipCard = ({ item, index, delay, onFlipComplete }: FlipCardProps) => {
  19. const animValue = useRef(new Animated.Value(0)).current;
  20. useEffect(() => {
  21. Animated.sequence([
  22. Animated.delay(delay),
  23. Animated.timing(animValue, {
  24. toValue: 180, // We'll map 0-180 to the flip
  25. duration: 600, // Flip duration
  26. useNativeDriver: true,
  27. })
  28. ]).start(() => {
  29. onFlipComplete && onFlipComplete();
  30. });
  31. }, [delay]);
  32. // Front (Back Image) Interpolation: 0 -> 90 derived from 0->180 range
  33. // Actually standard flip:
  34. // Front: 0deg to 180deg (starts visible, goes back)
  35. // Back: 180deg to 360deg (starts invisible back, goes front)
  36. // Uniapp Logic:
  37. // Back (Card Back): rotateY 0deg -> 90deg (Hide)
  38. // Front (Result): rotateY -90deg -> 0deg (Show)
  39. // Let's use animValue 0 -> 1.
  40. // 0 -> 0.5: Back rotates 0 -> 90.
  41. // 0.5 -> 1: Front rotates -90 -> 0.
  42. const backStyle = {
  43. transform: [
  44. {
  45. rotateY: animValue.interpolate({
  46. inputRange: [0, 90],
  47. outputRange: ['0deg', '90deg'],
  48. extrapolate: 'clamp',
  49. })
  50. }
  51. ],
  52. opacity: animValue.interpolate({
  53. inputRange: [0, 89, 90],
  54. outputRange: [1, 1, 0], // Hide when 90 to prevent z-fighting/artifacts
  55. extrapolate: 'clamp',
  56. })
  57. };
  58. const frontStyle = {
  59. transform: [
  60. {
  61. rotateY: animValue.interpolate({
  62. inputRange: [90, 180],
  63. outputRange: ['-90deg', '0deg'], // or 270 to 360
  64. extrapolate: 'clamp',
  65. })
  66. }
  67. ],
  68. opacity: animValue.interpolate({
  69. inputRange: [0, 90, 91],
  70. outputRange: [0, 0, 1],
  71. extrapolate: 'clamp',
  72. })
  73. };
  74. return (
  75. <View style={styles.cardContainer}>
  76. {/* Card Back (Initially Visible) */}
  77. <Animated.View style={[styles.cardFace, backStyle]}>
  78. <Image
  79. source={{ uri: Images.box.back }}
  80. style={{ width: '100%', height: '100%' }}
  81. contentFit="contain"
  82. />
  83. </Animated.View>
  84. {/* Result Face (Initially Hidden) */}
  85. <Animated.View style={[styles.cardFace, frontStyle]}>
  86. <TransCard
  87. item={item}
  88. width={ITEM_WIDTH - 10} // Padding inside
  89. height={ITEM_HEIGHT - 10}
  90. fill={0}
  91. imageWidth={(ITEM_WIDTH - 20) * 0.8}
  92. imageHeight={(ITEM_HEIGHT - 20) * 0.7}
  93. />
  94. </Animated.View>
  95. </View>
  96. );
  97. };
  98. interface LotteryGridProps {
  99. results: any[];
  100. onFinish: () => void;
  101. }
  102. export default function LotteryGrid({ results, onFinish }: LotteryGridProps) {
  103. // Determine staggered delay
  104. // 10 items.
  105. // We want them to flip sequentially or semi-sequentially.
  106. // Uniapp likely does it index based.
  107. const [completedCount, setCompletedCount] = useState(0);
  108. const handleFlipComplete = () => {
  109. setCompletedCount(prev => {
  110. const newCount = prev + 1;
  111. if (newCount === results.length) {
  112. // All done
  113. setTimeout(onFinish, 1000); // Wait a bit before finish
  114. }
  115. return newCount;
  116. });
  117. };
  118. return (
  119. <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
  120. <View style={styles.grid}>
  121. {results.map((item, index) => (
  122. <FlipCard
  123. key={index}
  124. item={item}
  125. index={index}
  126. delay={index * 200} // 200ms stagger
  127. onFlipComplete={handleFlipComplete}
  128. />
  129. ))}
  130. </View>
  131. </ScrollView>
  132. );
  133. }
  134. const styles = StyleSheet.create({
  135. container: {
  136. flex: 1,
  137. width: '100%',
  138. },
  139. scrollContent: {
  140. paddingTop: 20, // Reduced from 100 to avoid being too low (parent has margin)
  141. paddingBottom: 150, // Increased to avoid overlapping with absolute bottom button
  142. },
  143. grid: {
  144. flexDirection: 'row',
  145. flexWrap: 'wrap',
  146. justifyContent: 'center',
  147. },
  148. cardContainer: {
  149. width: ITEM_WIDTH,
  150. height: ITEM_HEIGHT,
  151. alignItems: 'center',
  152. justifyContent: 'center',
  153. marginVertical: 10,
  154. },
  155. cardFace: {
  156. position: 'absolute',
  157. width: '100%',
  158. height: '100%',
  159. alignItems: 'center',
  160. justifyContent: 'center',
  161. backfaceVisibility: 'hidden', // Android support varies, relying on opacity/rotation logic
  162. }
  163. });