活动列表添加无限滚动

This commit is contained in:
Vegas
2025-11-11 16:58:37 +07:00
parent f8413ca133
commit 01f1c61918

View File

@@ -1,10 +1,20 @@
import React, { useState } from 'react'; import React, { useState, useRef } from 'react';
import { ThemedView } from "@/components/themed-view"; import {
import { Text, SafeAreaView, StyleSheet, View, FlatList, TouchableOpacity, Dimensions, RefreshControl, Image } from "react-native"; Text,
SafeAreaView,
StyleSheet,
View,
FlatList,
TouchableOpacity,
Dimensions,
Image,
NativeSyntheticEvent,
NativeScrollEvent
} from "react-native";
const screenWidth = Dimensions.get('window').width; const screenWidth = Dimensions.get('window').width;
// 分类列表 - 添加图标emoji扩展到12个 // 分类列表
const categoryList = [ const categoryList = [
{ id: '0', title: '捕鱼', icon: '🎣' }, { id: '0', title: '捕鱼', icon: '🎣' },
{ id: '1', title: '真人', icon: '👤' }, { id: '1', title: '真人', icon: '👤' },
@@ -20,107 +30,83 @@ const categoryList = [
{ id: '11', title: '其他', icon: '⭐' }, { id: '11', title: '其他', icon: '⭐' },
]; ];
// 生成随机数量的游戏列表1-11个 // 随机生成游戏数据
const generateRandomGames = (categoryId: string, baseName: string): Array<{id: string, name: string, hot: boolean, image: string}> => { const generateRandomGames = (categoryId: string, baseName: string) => {
const count = Math.floor(Math.random() * 11) + 1; // 1-11个随机数量 const count = Math.floor(Math.random() * 11) + 1;
const games = [];
const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版']; const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版'];
// 模拟图片URL使用picsum.photos随机图片服务
const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C']; const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C'];
for (let i = 0; i < count; i++) { return Array.from({ length: count }).map((_, i) => {
const colorIndex = Math.floor(Math.random() * colors.length); const color = colors[Math.floor(Math.random() * colors.length)];
games.push({ return {
id: `${categoryId}-${i + 1}`, id: `${categoryId}-${i}`,
name: `${baseName}${gameTypes[i % gameTypes.length]}`, name: `${baseName}${gameTypes[i % gameTypes.length]}`,
hot: Math.random() > 0.6, // 40%概率为热门 hot: Math.random() > 0.6,
image: `https://via.placeholder.com/120x80/${colors[colorIndex]}/ffffff?text=Game+${i + 1}`, image: `https://via.placeholder.com/120x80/${color}/ffffff?text=Game+${i + 1}`,
}); };
} });
return games;
}; };
// 模拟右侧列表数据 - 随机数量扩展到12个分类 // 模拟数据
const mockData: Record<string, Array<{id: string, name: string, hot: boolean, image: string}>> = { const mockData: Record<string, Array<{id: string, name: string, hot: boolean, image: string}>> = {};
'0': generateRandomGames('0', '捕鱼'), categoryList.forEach(cat => {
'1': generateRandomGames('1', '真人百家乐'), mockData[cat.id] = generateRandomGames(cat.id, cat.title);
'2': generateRandomGames('2', '链上竞猜'), });
'3': generateRandomGames('3', '棋牌对战'),
'4': generateRandomGames('4', '老虎机'),
'5': generateRandomGames('5', '体育竞猜'),
'6': generateRandomGames('6', '彩票'),
'7': generateRandomGames('7', '电竞对战'),
'8': generateRandomGames('8', '赛车竞速'),
'9': generateRandomGames('9', '街机游戏'),
'10': generateRandomGames('10', '桌面游戏'),
'11': generateRandomGames('11', '特色游戏'),
};
type LeftItemProps = {
id: string;
title: string;
icon: string;
isSelected: boolean;
onPress: () => void;
};
type RightItemProps = {
name: string;
hot: boolean;
image: string;
};
export default function ActivityScreen() { export default function ActivityScreen() {
const [selectedId, setSelectedId] = useState('0'); const [selectedId, setSelectedId] = useState('0');
const [refreshingLeft, setRefreshingLeft] = useState(false); const [isSwitching, setIsSwitching] = useState(false);
const [refreshingRight, setRefreshingRight] = useState(false); const rightListRef = useRef<FlatList>(null);
// 左侧下拉刷新 // 支持循环切换分类
const onRefreshLeft = () => { const handleCategorySwitch = (direction: 'prev' | 'next') => {
setRefreshingLeft(true); const currentIndex = categoryList.findIndex(item => item.id === selectedId);
// 模拟刷新延迟 const total = categoryList.length;
setTimeout(() => { let newIndex = direction === 'next' ? (currentIndex + 1) % total : (currentIndex - 1 + total) % total;
setRefreshingLeft(false);
}, 1000); // 切换分类
setSelectedId(categoryList[newIndex].id);
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
}; };
// 右侧下拉刷新 // 实时检测滚动边界 + 循环切换
const onRefreshRight = () => { const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
setRefreshingRight(true); const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
// 重新生成当前分类的数据 const y = contentOffset.y;
const categoryNames: Record<string, string> = { const layoutHeight = layoutMeasurement.height;
'0': '捕鱼', '1': '真人百家乐', '2': '链上竞猜', '3': '棋牌对战', const contentHeight = contentSize.height;
'4': '老虎机', '5': '体育竞猜', '6': '彩票', '7': '电竞对战',
'8': '赛车竞速', '9': '街机游戏', '10': '桌面游戏', '11': '特色游戏' if (isSwitching) return;
};
mockData[selectedId] = generateRandomGames(selectedId, categoryNames[selectedId]); // 滑到顶部并继续下拉
setTimeout(() => { if (y < -40) {
setRefreshingRight(false); setIsSwitching(true);
}, 1000); handleCategorySwitch('prev');
setTimeout(() => setIsSwitching(false), 600);
}
// 滑到底部并继续上推
if (contentHeight <= layoutHeight ? y > 40 : y + layoutHeight - contentHeight > 40) {
setIsSwitching(true);
handleCategorySwitch('next');
setTimeout(() => setIsSwitching(false), 600);
}
}; };
const LeftItem = ({ id, title, icon, isSelected, onPress }: LeftItemProps) => ( const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
<TouchableOpacity <TouchableOpacity
style={[styles.leftItem, isSelected && styles.leftItemActive]} style={[styles.leftItem, isSelected && styles.leftItemActive]}
onPress={onPress} onPress={onPress}
> >
<Text style={styles.leftIcon}>{icon}</Text> <Text style={styles.leftIcon}>{icon}</Text>
<Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}> <Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}>{title}</Text>
{title}
</Text>
{isSelected && <View style={styles.activeIndicator} />} {isSelected && <View style={styles.activeIndicator} />}
</TouchableOpacity> </TouchableOpacity>
); );
const RightItem = ({ name, hot, image }: RightItemProps) => ( const RightItem = ({ name, hot, image }: any) => (
<View style={styles.rightItem}> <View style={styles.rightItem}>
<Image <Image source={{ uri: image }} style={styles.gameImage} resizeMode="cover" />
source={{ uri: image }}
style={styles.gameImage}
resizeMode="cover"
/>
<View style={styles.rightItemInfo}> <View style={styles.rightItemInfo}>
<View style={styles.rightItemContent}> <View style={styles.rightItemContent}>
<Text style={styles.rightItemTitle} numberOfLines={1}>{name}</Text> <Text style={styles.rightItemTitle} numberOfLines={1}>{name}</Text>
@@ -133,9 +119,9 @@ export default function ActivityScreen() {
return ( return (
<SafeAreaView style={styles.safeAreaContainer}> <SafeAreaView style={styles.safeAreaContainer}>
<ThemedView style={styles.container}> <View style={styles.container}>
<View style={styles.content}> <View style={styles.content}>
{/* 左侧菜单 */} {/* 左侧分类 */}
<View style={styles.leftWrapper}> <View style={styles.leftWrapper}>
<FlatList <FlatList
data={categoryList} data={categoryList}
@@ -150,55 +136,35 @@ export default function ActivityScreen() {
)} )}
keyExtractor={item => item.id} keyExtractor={item => item.id}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshingLeft}
onRefresh={onRefreshLeft}
colors={['#1890ff']}
tintColor="#1890ff"
/>
}
/> />
</View> </View>
{/* 右侧列表 */} {/* 右侧内容 */}
<View style={styles.contentRight}> <View style={styles.contentRight}>
<FlatList <FlatList
ref={rightListRef}
data={mockData[selectedId] || []} data={mockData[selectedId] || []}
renderItem={({ item }) => ( renderItem={({ item }) => (
<RightItem name={item.name} hot={item.hot} image={item.image} /> <RightItem name={item.name} hot={item.hot} image={item.image} />
)} )}
keyExtractor={item => item.id} keyExtractor={item => item.id}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
bounces={true}
scrollEventThrottle={16}
onScroll={handleScroll}
contentContainerStyle={styles.rightListContent} contentContainerStyle={styles.rightListContent}
refreshControl={
<RefreshControl
refreshing={refreshingRight}
onRefresh={onRefreshRight}
colors={['#1890ff']}
tintColor="#1890ff"
/>
}
/> />
</View> </View>
</View> </View>
</ThemedView> </View>
</SafeAreaView> </SafeAreaView>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeAreaContainer: { safeAreaContainer: { flex: 1, backgroundColor: '#f5f5f5' },
flex: 1, container: { flex: 1 },
backgroundColor: '#f5f5f5', content: { flex: 1, flexDirection: 'row' },
},
container: {
flex: 1,
},
content: {
flex: 1,
flexDirection: 'row',
},
leftWrapper: { leftWrapper: {
width: 90, width: 90,
backgroundColor: '#fff', backgroundColor: '#fff',
@@ -212,22 +178,10 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
position: 'relative', position: 'relative',
}, },
leftItemActive: { leftItemActive: { backgroundColor: '#f0f0f0' },
backgroundColor: '#f0f0f0', leftIcon: { fontSize: 24, marginBottom: 4 },
}, leftTitle: { fontSize: 12, color: '#666', textAlign: 'center' },
leftIcon: { leftTitleActive: { color: '#1890ff', fontWeight: '600' },
fontSize: 24,
marginBottom: 4,
},
leftTitle: {
fontSize: 12,
color: '#666',
textAlign: 'center',
},
leftTitleActive: {
color: '#1890ff',
fontWeight: '600',
},
activeIndicator: { activeIndicator: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
@@ -239,13 +193,8 @@ const styles = StyleSheet.create({
borderTopRightRadius: 2, borderTopRightRadius: 2,
borderBottomRightRadius: 2, borderBottomRightRadius: 2,
}, },
contentRight: { contentRight: { flex: 1, backgroundColor: '#f5f5f5' },
flex: 1, rightListContent: { padding: 12, paddingBottom: 20 },
backgroundColor: '#f5f5f5',
},
rightListContent: {
padding: 12,
},
rightItem: { rightItem: {
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 8, borderRadius: 8,
@@ -266,35 +215,10 @@ const styles = StyleSheet.create({
marginRight: 12, marginRight: 12,
backgroundColor: '#f0f0f0', backgroundColor: '#f0f0f0',
}, },
rightItemInfo: { rightItemInfo: { flex: 1, justifyContent: 'center' },
flex: 1, rightItemContent: { flexDirection: 'row', alignItems: 'center', marginBottom: 6 },
justifyContent: 'center', rightItemTitle: { fontSize: 15, fontWeight: '600', color: '#333', marginRight: 8, flex: 1 },
}, hotBadge: { backgroundColor: '#ff4d4f', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4 },
rightItemContent: { hotText: { color: '#fff', fontSize: 10, fontWeight: '600' },
flexDirection: 'row', rightItemDesc: { fontSize: 13, color: '#999' },
alignItems: 'center',
marginBottom: 6,
},
rightItemTitle: {
fontSize: 15,
fontWeight: '600',
color: '#333',
marginRight: 8,
flex: 1,
},
hotBadge: {
backgroundColor: '#ff4d4f',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
hotText: {
color: '#fff',
fontSize: 10,
fontWeight: '600',
},
rightItemDesc: {
fontSize: 13,
color: '#999',
},
}); });