Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bbaf37b2c | ||
|
|
8090c60142 | ||
|
|
3703b20ef5 | ||
|
|
01f1c61918 | ||
|
|
f8413ca133 | ||
|
|
a8e67a5403 | ||
|
|
e384316b44 |
@@ -1,26 +1,303 @@
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { Text, SafeAreaView, StyleSheet } from "react-native";
|
||||
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 适配顶部刘海
|
||||
<SafeAreaView style={styles.safeAreaContainer}> /
|
||||
<ThemedView><Text>活动</Text></ThemedView>
|
||||
<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: {
|
||||
// 确保 SafeAreaView 占据整个屏幕
|
||||
flex: 1,
|
||||
// 您可能还需要在这里设置背景颜色,以确保状态栏区域的颜色正确
|
||||
backgroundColor: 'pink'
|
||||
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' },
|
||||
});
|
||||
@@ -1,3 +1,9 @@
|
||||
// App.js 或 index.js 文件的最顶部
|
||||
if (typeof setImmediate === 'undefined') {
|
||||
global.setImmediate = function(callback, ...args) {
|
||||
return setTimeout(callback, 0, ...args);
|
||||
};
|
||||
}
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -31,6 +31,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-swiper": "^1.6.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
@@ -10316,7 +10317,6 @@
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
@@ -10328,7 +10328,6 @@
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
@@ -10619,6 +10618,15 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-swiper": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-swiper/-/react-native-swiper-1.6.0.tgz",
|
||||
"integrity": "sha512-OnkTTZi+9uZUgy0uz1I9oYDhCU3z36lZn+LFsk9FXPRelxb/KeABzvPs3r3SrHWy1aA67KGtSFj0xNK2QD0NJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-web": {
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-swiper": "^1.6.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.5.1"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user