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