2025-11-11 16:58:37 +07:00
|
|
|
|
import React, { useState, useRef } from 'react';
|
2025-11-11 17:34:33 +07:00
|
|
|
|
import {
|
|
|
|
|
|
Text,
|
|
|
|
|
|
SafeAreaView,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
View,
|
|
|
|
|
|
FlatList,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
Dimensions,
|
|
|
|
|
|
Image,
|
|
|
|
|
|
NativeSyntheticEvent,
|
|
|
|
|
|
NativeScrollEvent,
|
|
|
|
|
|
LayoutChangeEvent // 导入 LayoutChangeEvent 类型
|
2025-11-11 16:58:37 +07:00
|
|
|
|
} from "react-native";
|
2025-11-11 15:54:13 +07:00
|
|
|
|
|
2025-11-11 15:34:50 +07:00
|
|
|
|
const screenWidth = Dimensions.get('window').width;
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
// 分类列表
|
2025-11-11 15:54:13 +07:00
|
|
|
|
const categoryList = [
|
|
|
|
|
|
{ id: '0', title: '捕鱼', icon: '🎣' },
|
|
|
|
|
|
{ id: '1', title: '真人', icon: '👤' },
|
|
|
|
|
|
{ id: '2', title: '区块链', icon: '⛓️' },
|
|
|
|
|
|
{ id: '3', title: '棋牌', icon: '🎴' },
|
|
|
|
|
|
{ id: '4', title: '电子', icon: '🎰' },
|
|
|
|
|
|
{ id: '5', title: '体育', icon: '⚽' },
|
|
|
|
|
|
{ id: '6', title: '彩票', icon: '🎫' },
|
|
|
|
|
|
{ id: '7', title: '电竞', icon: '🎮' },
|
|
|
|
|
|
{ id: '8', title: '赛车', icon: '🏎️' },
|
|
|
|
|
|
{ id: '9', title: '街机', icon: '🕹️' },
|
|
|
|
|
|
{ id: '10', title: '桌游', icon: '🎲' },
|
|
|
|
|
|
{ id: '11', title: '其他', icon: '⭐' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
// 随机生成游戏数据
|
|
|
|
|
|
const generateRandomGames = (categoryId: string, baseName: string) => {
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 调整随机数,使其更容易出现空列表或少量item,以测试优化效果
|
|
|
|
|
|
// (Math.random() * 6) + 0 意味着可能产生 0 到 5 个
|
|
|
|
|
|
const count = Math.floor(Math.random() * 6);
|
2025-11-11 15:54:13 +07:00
|
|
|
|
const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版'];
|
|
|
|
|
|
const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C'];
|
2025-11-11 15:34:50 +07:00
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
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,
|
|
|
|
|
|
image: `https://via.placeholder.com/120x80/${color}/ffffff?text=Game+${i + 1}`,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
2025-11-11 15:54:13 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
// 模拟数据
|
2025-11-11 17:34:33 +07:00
|
|
|
|
const mockData: Record<string, Array<{ id: string, name: string, hot: boolean, image: string }>> = {};
|
2025-11-11 16:58:37 +07:00
|
|
|
|
categoryList.forEach(cat => {
|
|
|
|
|
|
mockData[cat.id] = generateRandomGames(cat.id, cat.title);
|
|
|
|
|
|
});
|
2025-11-11 15:34:50 +07:00
|
|
|
|
|
2025-11-06 16:32:15 +07:00
|
|
|
|
export default function ActivityScreen() {
|
2025-11-11 15:54:13 +07:00
|
|
|
|
const [selectedId, setSelectedId] = useState('0');
|
2025-11-11 16:58:37 +07:00
|
|
|
|
const [isSwitching, setIsSwitching] = useState(false);
|
|
|
|
|
|
const rightListRef = useRef<FlatList>(null);
|
2025-11-11 15:54:13 +07:00
|
|
|
|
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 【新增】State,用于存储右侧列表容器的实际高度
|
|
|
|
|
|
const [rightListHeight, setRightListHeight] = useState(0);
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
// 支持循环切换分类
|
|
|
|
|
|
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);
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 切换时重置滚动,避免新列表加载时停在奇怪的位置
|
2025-11-11 16:58:37 +07:00
|
|
|
|
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
2025-11-11 15:54:13 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
// 实时检测滚动边界 + 循环切换
|
|
|
|
|
|
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
|
|
|
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
|
|
|
|
|
const y = contentOffset.y;
|
|
|
|
|
|
const layoutHeight = layoutMeasurement.height;
|
|
|
|
|
|
const contentHeight = contentSize.height;
|
|
|
|
|
|
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 如果正在切换中,则锁定,防止短时间内连续触发
|
2025-11-11 16:58:37 +07:00
|
|
|
|
if (isSwitching) return;
|
|
|
|
|
|
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 滑到顶部并继续下拉(-40 是一个触发阈值)
|
2025-11-11 16:58:37 +07:00
|
|
|
|
if (y < -40) {
|
|
|
|
|
|
setIsSwitching(true);
|
|
|
|
|
|
handleCategorySwitch('prev');
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 延迟重置isSwitching状态,给动画和数据加载留出时间
|
2025-11-11 16:58:37 +07:00
|
|
|
|
setTimeout(() => setIsSwitching(false), 600);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 滑到底部并继续上推
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// contentHeight <= layoutHeight 检查内容是否填满
|
|
|
|
|
|
// (如果没填满) y > 40 检查是否在空白处上拉
|
|
|
|
|
|
// (如果已填满) y + layoutHeight - contentHeight > 40 检查是否滚动到底部并继续上拉
|
2025-11-11 16:58:37 +07:00
|
|
|
|
if (contentHeight <= layoutHeight ? y > 40 : y + layoutHeight - contentHeight > 40) {
|
|
|
|
|
|
setIsSwitching(true);
|
|
|
|
|
|
handleCategorySwitch('next');
|
|
|
|
|
|
setTimeout(() => setIsSwitching(false), 600);
|
|
|
|
|
|
}
|
2025-11-11 15:54:13 +07:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-11 17:34:33 +07:00
|
|
|
|
// 【新增】onLayout 事件处理函数
|
|
|
|
|
|
// 当右侧容器布局时,获取其高度
|
|
|
|
|
|
const onRightListLayout = (event: LayoutChangeEvent) => {
|
|
|
|
|
|
const { height } = event.nativeEvent.layout;
|
|
|
|
|
|
setRightListHeight(height);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={[styles.leftItem, isSelected && styles.leftItemActive]}
|
2025-11-11 15:54:13 +07:00
|
|
|
|
onPress={onPress}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text style={styles.leftIcon}>{icon}</Text>
|
2025-11-11 16:58:37 +07:00
|
|
|
|
<Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}>{title}</Text>
|
2025-11-11 15:54:13 +07:00
|
|
|
|
{isSelected && <View style={styles.activeIndicator} />}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
const RightItem = ({ name, hot, image }: any) => (
|
2025-11-11 15:54:13 +07:00
|
|
|
|
<View style={styles.rightItem}>
|
2025-11-11 16:58:37 +07:00
|
|
|
|
<Image source={{ uri: image }} style={styles.gameImage} resizeMode="cover" />
|
2025-11-11 15:54:13 +07:00
|
|
|
|
<View style={styles.rightItemInfo}>
|
|
|
|
|
|
<View style={styles.rightItemContent}>
|
|
|
|
|
|
<Text style={styles.rightItemTitle} numberOfLines={1}>{name}</Text>
|
|
|
|
|
|
{hot && <View style={styles.hotBadge}><Text style={styles.hotText}>HOT</Text></View>}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
<Text style={styles.rightItemDesc}>精彩游戏,等你来玩</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-06 16:32:15 +07:00
|
|
|
|
return (
|
2025-11-11 15:34:50 +07:00
|
|
|
|
<SafeAreaView style={styles.safeAreaContainer}>
|
2025-11-11 16:58:37 +07:00
|
|
|
|
<View style={styles.container}>
|
2025-11-11 15:54:13 +07:00
|
|
|
|
<View style={styles.content}>
|
2025-11-11 16:58:37 +07:00
|
|
|
|
{/* 左侧分类 */}
|
2025-11-11 15:34:50 +07:00
|
|
|
|
<View style={styles.leftWrapper}>
|
|
|
|
|
|
<FlatList
|
2025-11-11 15:54:13 +07:00
|
|
|
|
data={categoryList}
|
|
|
|
|
|
renderItem={({ item }) => (
|
2025-11-11 16:58:37 +07:00
|
|
|
|
<LeftItem
|
2025-11-11 15:54:13 +07:00
|
|
|
|
id={item.id}
|
|
|
|
|
|
title={item.title}
|
|
|
|
|
|
icon={item.icon}
|
|
|
|
|
|
isSelected={selectedId === item.id}
|
|
|
|
|
|
onPress={() => setSelectedId(item.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-11-11 15:34:50 +07:00
|
|
|
|
keyExtractor={item => item.id}
|
2025-11-11 15:54:13 +07:00
|
|
|
|
showsVerticalScrollIndicator={false}
|
2025-11-11 15:34:50 +07:00
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
|
2025-11-11 16:58:37 +07:00
|
|
|
|
{/* 右侧内容 */}
|
2025-11-11 17:34:33 +07:00
|
|
|
|
{/* 【优化点 1】给容器添加 onLayout 来获取高度 */}
|
|
|
|
|
|
<View style={styles.contentRight} onLayout={onRightListLayout}>
|
|
|
|
|
|
{/* 只有在获取到高度后才渲染 FlatList,确保 minHeight 初始值正确 */}
|
|
|
|
|
|
{rightListHeight > 0 && (
|
|
|
|
|
|
<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 (默认值)
|
|
|
|
|
|
bounces={true}
|
|
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
|
|
onScroll={handleScroll}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化点 2】为 iOS 添加,使其在内容不足时也能反弹
|
|
|
|
|
|
alwaysBounceVertical={true}
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化点 3】设置 contentContainerStyle
|
|
|
|
|
|
contentContainerStyle={[
|
|
|
|
|
|
styles.rightListContent,
|
|
|
|
|
|
{
|
|
|
|
|
|
// 确保内容容器的最小高度
|
|
|
|
|
|
// 始终比 FlatList 视图本身高 1 像素
|
|
|
|
|
|
minHeight: rightListHeight + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-11-11 15:34:50 +07:00
|
|
|
|
</View>
|
|
|
|
|
|
</View>
|
2025-11-11 16:58:37 +07:00
|
|
|
|
</View>
|
2025-11-11 10:39:26 +07:00
|
|
|
|
</SafeAreaView>
|
2025-11-11 15:54:13 +07:00
|
|
|
|
);
|
2025-11-06 16:32:15 +07:00
|
|
|
|
}
|
2025-11-11 10:39:26 +07:00
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
2025-11-11 16:58:37 +07:00
|
|
|
|
safeAreaContainer: { flex: 1, backgroundColor: '#f5f5f5' },
|
|
|
|
|
|
container: { flex: 1 },
|
|
|
|
|
|
content: { flex: 1, flexDirection: 'row' },
|
2025-11-11 15:34:50 +07:00
|
|
|
|
leftWrapper: {
|
2025-11-11 15:54:13 +07:00
|
|
|
|
width: 90,
|
|
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
|
|
borderRightWidth: 1,
|
|
|
|
|
|
borderRightColor: '#e0e0e0',
|
2025-11-11 15:34:50 +07:00
|
|
|
|
},
|
|
|
|
|
|
leftItem: {
|
2025-11-11 15:54:13 +07:00
|
|
|
|
paddingVertical: 18,
|
|
|
|
|
|
paddingHorizontal: 8,
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
position: 'relative',
|
|
|
|
|
|
},
|
2025-11-11 16:58:37 +07:00
|
|
|
|
leftItemActive: { backgroundColor: '#f0f0f0' },
|
|
|
|
|
|
leftIcon: { fontSize: 24, marginBottom: 4 },
|
|
|
|
|
|
leftTitle: { fontSize: 12, color: '#666', textAlign: 'center' },
|
|
|
|
|
|
leftTitleActive: { color: '#1890ff', fontWeight: '600' },
|
2025-11-11 15:54:13 +07:00
|
|
|
|
activeIndicator: {
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
top: '50%',
|
|
|
|
|
|
marginTop: -12,
|
|
|
|
|
|
width: 3,
|
|
|
|
|
|
height: 24,
|
|
|
|
|
|
backgroundColor: '#1890ff',
|
|
|
|
|
|
borderTopRightRadius: 2,
|
|
|
|
|
|
borderBottomRightRadius: 2,
|
2025-11-11 15:34:50 +07:00
|
|
|
|
},
|
2025-11-11 16:58:37 +07:00
|
|
|
|
contentRight: { flex: 1, backgroundColor: '#f5f5f5' },
|
2025-11-11 17:34:33 +07:00
|
|
|
|
rightListContent: {
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
paddingBottom: 20
|
|
|
|
|
|
},
|
2025-11-11 15:54:13 +07:00
|
|
|
|
rightItem: {
|
|
|
|
|
|
backgroundColor: '#fff',
|
|
|
|
|
|
borderRadius: 8,
|
|
|
|
|
|
padding: 12,
|
|
|
|
|
|
marginBottom: 12,
|
|
|
|
|
|
shadowColor: '#000',
|
|
|
|
|
|
shadowOffset: { width: 0, height: 1 },
|
|
|
|
|
|
shadowOpacity: 0.05,
|
|
|
|
|
|
shadowRadius: 2,
|
|
|
|
|
|
elevation: 2,
|
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
alignItems: 'center',
|
2025-11-11 15:34:50 +07:00
|
|
|
|
},
|
2025-11-11 15:54:13 +07:00
|
|
|
|
gameImage: {
|
|
|
|
|
|
width: 120,
|
|
|
|
|
|
height: 80,
|
|
|
|
|
|
borderRadius: 6,
|
|
|
|
|
|
marginRight: 12,
|
|
|
|
|
|
backgroundColor: '#f0f0f0',
|
2025-11-11 15:34:50 +07:00
|
|
|
|
},
|
2025-11-11 16:58:37 +07:00
|
|
|
|
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' },
|
2025-11-11 17:34:33 +07:00
|
|
|
|
});
|