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

303 lines
10 KiB
TypeScript
Raw Permalink Normal View History

import React, { useState, useRef, useEffect } from 'react';
import {
Text,
SafeAreaView,
StyleSheet,
View,
FlatList,
TouchableOpacity,
Dimensions,
Image,
NativeSyntheticEvent,
NativeScrollEvent,
2025-11-11 17:53:29 +07:00
LayoutChangeEvent,
LayoutAnimation, // 动画
UIManager, // 动画
Platform // 动画
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 17:53:29 +07:00
// 为 Android 启用 LayoutAnimation (必须的样板代码)
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
// --- 数据模型和常量 ---
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:53:29 +07:00
// 产生 0 到 5 个 Item用于测试空白区域滚动
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
// 模拟数据
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-11 17:53:29 +07:00
// 定义左侧Item的固定高度用于 getItemLayout
const LEFT_ITEM_HEIGHT = 76;
2025-11-11 17:53:29 +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);
const leftListRef = useRef<FlatList>(null);
const [rightListHeight, setRightListHeight] = useState(0);
2025-11-11 17:53:29 +07:00
// 优化 1: 左侧列表自动滚动,确保选中项可见
useEffect(() => {
const newIndex = categoryList.findIndex(item => item.id === selectedId);
if (leftListRef.current && newIndex > -1) {
leftListRef.current.scrollToIndex({
index: newIndex,
animated: true,
2025-11-11 17:53:29 +07:00
viewPosition: 0.5 // 滚动到居中位置
});
}
2025-11-11 17:53:29 +07:00
}, [selectedId]);
2025-11-11 17:53:29 +07:00
// 处理分类切换的核心逻辑
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;
2025-11-11 17:53:29 +07:00
// 优化 3: 在 state 改变之前,配置下一次布局动画
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
2025-11-11 16:58:37 +07:00
// 切换分类
setSelectedId(categoryList[newIndex].id);
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
2025-11-11 15:54:13 +07:00
};
2025-11-11 17:53:29 +07:00
// 优化 2: 实时检测滚动边界 + 循环切换 (处理空白区域滚动)
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;
if (isSwitching) return;
// 滑到顶部并继续下拉
2025-11-11 16:58:37 +07:00
if (y < -40) {
setIsSwitching(true);
handleCategorySwitch('prev');
setTimeout(() => setIsSwitching(false), 600);
}
2025-11-11 17:53:29 +07:00
// 滑到底部并继续上推 (包含内容不足以填满屏幕的情况)
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:53:29 +07:00
// 获取右侧列表容器的高度 (用于 minHeight 优化)
const onRightListLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout;
setRightListHeight(height);
};
// 左侧Item组件
2025-11-11 16:58:37 +07:00
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>
2025-11-11 15:54:13 +07:00
);
// 右侧Item组件
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>
);
// 为 leftListRef 的 scrollToIndex 提供性能保证
const getItemLayout = (data: any, index: number) => ({
length: LEFT_ITEM_HEIGHT,
offset: LEFT_ITEM_HEIGHT * index,
index
});
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
ref={leftListRef}
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}
2025-11-11 17:53:29 +07:00
onPress={() => {
// 优化 3: 左侧点击时也触发动画
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setSelectedId(item.id);
}}
2025-11-11 15:54:13 +07:00
/>
)}
2025-11-11 15:34:50 +07:00
keyExtractor={item => item.id}
2025-11-11 15:54:13 +07:00
showsVerticalScrollIndicator={false}
getItemLayout={getItemLayout} // 提高滚动性能
2025-11-11 15:34:50 +07:00
/>
</View>
2025-11-11 16:58:37 +07:00
{/* 右侧内容 */}
<View style={styles.contentRight} onLayout={onRightListLayout}>
2025-11-11 17:53:29 +07:00
{/* 只有在获取到高度后才渲染 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}
2025-11-11 17:53:29 +07:00
alwaysBounceVertical={true} // 确保内容不足时也能滚动 (iOS)
contentContainerStyle={[
styles.rightListContent,
{
2025-11-11 17:53:29 +07:00
// 优化 2: 确保内容容器的最小高度,实现空白区域滚动
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
},
2025-11-11 17:53:29 +07:00
// 严格控制Item高度用于 getItemLayout
leftItemContainer: {
2025-11-11 17:53:29 +07:00
height: LEFT_ITEM_HEIGHT,
width: '100%',
},
2025-11-11 15:34:50 +07:00
leftItem: {
flex: 1, // 填满父容器
2025-11-11 15:54:13 +07:00
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, // (24 / 2)
2025-11-11 15:54:13 +07:00
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' },
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' },
});