Files
rn-app1/app/(tabs)/activity.tsx
2025-11-11 17:53:29 +07:00

303 lines
10 KiB
TypeScript
Raw Permalink 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,
LayoutAnimation, // 动画
UIManager, // 动画
Platform // 动画
} from "react-native";
const screenWidth = Dimensions.get('window').width;
// 为 Android 启用 LayoutAnimation (必须的样板代码)
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
// --- 数据模型和常量 ---
// 分类列表
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) => {
// 产生 0 到 5 个 Item用于测试空白区域滚动
const count = Math.floor(Math.random() * 6);
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的固定高度用于 getItemLayout
const LEFT_ITEM_HEIGHT = 76;
export default function ActivityScreen() {
const [selectedId, setSelectedId] = useState('0');
const [isSwitching, setIsSwitching] = useState(false);
const rightListRef = useRef<FlatList>(null);
const leftListRef = useRef<FlatList>(null);
const [rightListHeight, setRightListHeight] = useState(0);
// 优化 1: 左侧列表自动滚动,确保选中项可见
useEffect(() => {
const newIndex = categoryList.findIndex(item => item.id === selectedId);
if (leftListRef.current && newIndex > -1) {
leftListRef.current.scrollToIndex({
index: newIndex,
animated: true,
viewPosition: 0.5 // 滚动到居中位置
});
}
}, [selectedId]);
// 处理分类切换的核心逻辑
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;
// 优化 3: 在 state 改变之前,配置下一次布局动画
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
// 切换分类
setSelectedId(categoryList[newIndex].id);
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
};
// 优化 2: 实时检测滚动边界 + 循环切换 (处理空白区域滚动)
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);
}
};
// 获取右侧列表容器的高度 (用于 minHeight 优化)
const onRightListLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
setRightListHeight(height);
};
// 左侧Item组件
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
<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={() => {
// 优化 3: 左侧点击时也触发动画
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setSelectedId(item.id);
}}
/>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
getItemLayout={getItemLayout} // 提高滚动性能
/>
</View>
{/* 右侧内容 */}
<View style={styles.contentRight} onLayout={onRightListLayout}>
{/* 只有在获取到高度后才渲染 FlatList */}
{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}
alwaysBounceVertical={true} // 确保内容不足时也能滚动 (iOS)
contentContainerStyle={[
styles.rightListContent,
{
// 优化 2: 确保内容容器的最小高度,实现空白区域滚动
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高度用于 getItemLayout
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' },
});