Files
rn-app1/app/(tabs)/activity.tsx

301 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useRef, useEffect } from 'react';
import {
Text,
SafeAreaView,
StyleSheet,
View,
FlatList,
TouchableOpacity,
Dimensions,
Image,
NativeSyntheticEvent,
NativeScrollEvent,
LayoutChangeEvent
} from "react-native";
const screenWidth = Dimensions.get('window').width;
// 分类列表
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: '⭐' },
];
// 随机生成游戏数据
const generateRandomGames = (categoryId: string, baseName: string) => {
// 调整随机数使其更容易出现空列表或少量item以测试优化效果
const count = Math.floor(Math.random() * 6); // 产生 0 到 5 个
const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版'];
const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C'];
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}`,
};
});
};
// 模拟数据
const mockData: Record<string, Array<{ id: string, name: string, hot: boolean, image: string }>> = {};
categoryList.forEach(cat => {
mockData[cat.id] = generateRandomGames(cat.id, cat.title);
});
// 定义左侧Item的固定高度
// 样式中: paddingVertical(18*2) + icon(24) + iconMargin(4) + title(12) 估算
// 我们在样式中直接指定为 76
const LEFT_ITEM_HEIGHT = 76;
export default function ActivityScreen() {
const [selectedId, setSelectedId] = useState('0');
const [isSwitching, setIsSwitching] = useState(false);
const rightListRef = useRef<FlatList>(null);
// 创建左侧列表的ref
const leftListRef = useRef<FlatList>(null);
// State用于存储右侧列表容器的实际高度
const [rightListHeight, setRightListHeight] = useState(0);
// Slogan当selectedId变化时自动滚动左侧列表
useEffect(() => {
// 1. 找到当前 selectedId 对应的索引
const newIndex = categoryList.findIndex(item => item.id === selectedId);
// 2. 如果找到了 (index > -1) 并且 ref 已经准备好
if (leftListRef.current && newIndex > -1) {
// 3. 命令 FlatList 滚动到该索引
leftListRef.current.scrollToIndex({
index: newIndex,
animated: true,
// viewPosition: 0.5 会将item滚动到列表的中间位置体验更好
viewPosition: 0.5
});
}
}, [selectedId]); // 依赖项是 selectedId它变化时此Slogan执行
// 支持循环切换分类
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 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);
}
// 滑到底部并继续上推
// contentHeight <= layoutHeight 检查内容是否填满
if (contentHeight <= layoutHeight ? y > 40 : y + layoutHeight - contentHeight > 40) {
setIsSwitching(true);
handleCategorySwitch('next');
setTimeout(() => setIsSwitching(false), 600);
}
};
// onLayout 事件处理函数
const onRightListLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
setRightListHeight(height);
};
// 左侧Item组件
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
// 包裹在 View 中,并精确设置高度,确保 getItemLayout 的计算正确
<View style={styles.leftItemContainer}>
<TouchableOpacity
style={[styles.leftItem, isSelected && styles.leftItemActive]}
onPress={onPress}
>
<Text style={styles.leftIcon}>{icon}</Text>
<Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}>{title}</Text>
{isSelected && <View style={styles.activeIndicator} />}
</TouchableOpacity>
</View>
);
// 右侧Item组件
const RightItem = ({ name, hot, image }: any) => (
<View style={styles.rightItem}>
<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>
{hot && <View style={styles.hotBadge}><Text style={styles.hotText}>HOT</Text></View>}
</View>
<Text style={styles.rightItemDesc}></Text>
</View>
</View>
);
// 为 leftListRef 的 scrollToIndex 提供性能保证
const getItemLayout = (data: any, index: number) => ({
length: LEFT_ITEM_HEIGHT,
offset: LEFT_ITEM_HEIGHT * index,
index
});
return (
<SafeAreaView style={styles.safeAreaContainer}>
<View style={styles.container}>
<View style={styles.content}>
{/* 左侧分类 */}
<View style={styles.leftWrapper}>
<FlatList
ref={leftListRef}
data={categoryList}
renderItem={({ item }) => (
<LeftItem
id={item.id}
title={item.title}
icon={item.icon}
isSelected={selectedId === item.id}
onPress={() => setSelectedId(item.id)}
/>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
getItemLayout={getItemLayout} // 提高滚动性能
/>
</View>
{/* 右侧内容 */}
<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}
scrollEventThrottle={16}
onScroll={handleScroll}
// 为 iOS 添加,使其在内容不足时也能反弹
alwaysBounceVertical={true}
// 设置 contentContainerStyle
contentContainerStyle={[
styles.rightListContent,
{
// 确保内容容器的最小高度
// 始终比 FlatList 视图本身高 1 像素
minHeight: rightListHeight + 1
}
]}
/>
)}
</View>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeAreaContainer: { flex: 1, backgroundColor: '#f5f5f5' },
container: { flex: 1 },
content: { flex: 1, flexDirection: 'row' },
leftWrapper: {
width: 90,
backgroundColor: '#fff',
borderRightWidth: 1,
borderRightColor: '#e0e0e0',
},
// 一个容器严格控制Item高度
leftItemContainer: {
height: LEFT_ITEM_HEIGHT, // 严格高度
width: '100%',
},
leftItem: {
flex: 1, // 填满父容器
paddingHorizontal: 8,
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
},
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,
top: '50%',
marginTop: -12, // (24 / 2)
width: 3,
height: 24,
backgroundColor: '#1890ff',
borderTopRightRadius: 2,
borderBottomRightRadius: 2,
},
contentRight: { flex: 1, backgroundColor: '#f5f5f5' },
rightListContent: {
padding: 12,
paddingBottom: 20
},
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',
},
gameImage: {
width: 120,
height: 80,
borderRadius: 6,
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' },
});