滚动时让左侧菜单选中的item始终居中展示

This commit is contained in:
Vegas
2025-11-11 17:45:09 +07:00
parent 3703b20ef5
commit 8090c60142

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { import {
Text, Text,
SafeAreaView, SafeAreaView,
@@ -10,7 +10,7 @@ import {
Image, Image,
NativeSyntheticEvent, NativeSyntheticEvent,
NativeScrollEvent, NativeScrollEvent,
LayoutChangeEvent // 导入 LayoutChangeEvent 类型 LayoutChangeEvent
} from "react-native"; } from "react-native";
const screenWidth = Dimensions.get('window').width; const screenWidth = Dimensions.get('window').width;
@@ -34,8 +34,7 @@ const categoryList = [
// 随机生成游戏数据 // 随机生成游戏数据
const generateRandomGames = (categoryId: string, baseName: string) => { const generateRandomGames = (categoryId: string, baseName: string) => {
// 调整随机数使其更容易出现空列表或少量item以测试优化效果 // 调整随机数使其更容易出现空列表或少量item以测试优化效果
// (Math.random() * 6) + 0 意味着可能产生 0 到 5 个 const count = Math.floor(Math.random() * 6); // 产生 0 到 5 个
const count = Math.floor(Math.random() * 6);
const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版']; const gameTypes = ['经典版', '豪华版', '至尊版', '竞技版', '休闲版', '大师版', '传奇版', '极速版', '黄金版', '钻石版', '王者版'];
const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C']; const colors = ['4A90E2', 'E94B3C', '50C878', 'F39C12', '9B59B6', 'E74C3C'];
@@ -56,14 +55,40 @@ categoryList.forEach(cat => {
mockData[cat.id] = generateRandomGames(cat.id, cat.title); 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() { export default function ActivityScreen() {
const [selectedId, setSelectedId] = useState('0'); const [selectedId, setSelectedId] = useState('0');
const [isSwitching, setIsSwitching] = useState(false); const [isSwitching, setIsSwitching] = useState(false);
const rightListRef = useRef<FlatList>(null); const rightListRef = useRef<FlatList>(null);
// 【新增】State用于存储右侧列表容器的实际高度 // 创建左侧列表的ref
const leftListRef = useRef<FlatList>(null);
// State用于存储右侧列表容器的实际高度
const [rightListHeight, setRightListHeight] = useState(0); 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 handleCategorySwitch = (direction: 'prev' | 'next') => {
const currentIndex = categoryList.findIndex(item => item.id === selectedId); const currentIndex = categoryList.findIndex(item => item.id === selectedId);
@@ -72,7 +97,7 @@ export default function ActivityScreen() {
// 切换分类 // 切换分类
setSelectedId(categoryList[newIndex].id); setSelectedId(categoryList[newIndex].id);
// 切换时重置滚动,避免新列表加载时停在奇怪的位置 // 切换时重置滚动
rightListRef.current?.scrollToOffset({ offset: 0, animated: false }); rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
}; };
@@ -83,21 +108,17 @@ export default function ActivityScreen() {
const layoutHeight = layoutMeasurement.height; const layoutHeight = layoutMeasurement.height;
const contentHeight = contentSize.height; const contentHeight = contentSize.height;
// 如果正在切换中,则锁定,防止短时间内连续触发
if (isSwitching) return; if (isSwitching) return;
// 滑到顶部并继续下拉-40 是一个触发阈值) // 滑到顶部并继续下拉
if (y < -40) { if (y < -40) {
setIsSwitching(true); setIsSwitching(true);
handleCategorySwitch('prev'); handleCategorySwitch('prev');
// 延迟重置isSwitching状态给动画和数据加载留出时间
setTimeout(() => setIsSwitching(false), 600); setTimeout(() => setIsSwitching(false), 600);
} }
// 滑到底部并继续上推 // 滑到底部并继续上推
// contentHeight <= layoutHeight 检查内容是否填满 // contentHeight <= layoutHeight 检查内容是否填满
// (如果没填满) y > 40 检查是否在空白处上拉
// (如果已填满) y + layoutHeight - contentHeight > 40 检查是否滚动到底部并继续上拉
if (contentHeight <= layoutHeight ? y > 40 : y + layoutHeight - contentHeight > 40) { if (contentHeight <= layoutHeight ? y > 40 : y + layoutHeight - contentHeight > 40) {
setIsSwitching(true); setIsSwitching(true);
handleCategorySwitch('next'); handleCategorySwitch('next');
@@ -105,15 +126,17 @@ export default function ActivityScreen() {
} }
}; };
// 【新增】onLayout 事件处理函数 // onLayout 事件处理函数
// 当右侧容器布局时,获取其高度
const onRightListLayout = (event: LayoutChangeEvent) => { const onRightListLayout = (event: LayoutChangeEvent) => {
const { height } = event.nativeEvent.layout; const { height } = event.nativeEvent.layout;
setRightListHeight(height); setRightListHeight(height);
}; };
// 左侧Item组件
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => ( const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
// 包裹在 View 中,并精确设置高度,确保 getItemLayout 的计算正确
<View style={styles.leftItemContainer}>
<TouchableOpacity <TouchableOpacity
style={[styles.leftItem, isSelected && styles.leftItemActive]} style={[styles.leftItem, isSelected && styles.leftItemActive]}
onPress={onPress} onPress={onPress}
@@ -122,8 +145,10 @@ export default function ActivityScreen() {
<Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}>{title}</Text> <Text style={[styles.leftTitle, isSelected && styles.leftTitleActive]}>{title}</Text>
{isSelected && <View style={styles.activeIndicator} />} {isSelected && <View style={styles.activeIndicator} />}
</TouchableOpacity> </TouchableOpacity>
</View>
); );
// 右侧Item组件
const RightItem = ({ name, hot, image }: any) => ( const RightItem = ({ name, hot, image }: any) => (
<View style={styles.rightItem}> <View style={styles.rightItem}>
<Image source={{ uri: image }} style={styles.gameImage} resizeMode="cover" /> <Image source={{ uri: image }} style={styles.gameImage} resizeMode="cover" />
@@ -137,6 +162,13 @@ export default function ActivityScreen() {
</View> </View>
); );
// 为 leftListRef 的 scrollToIndex 提供性能保证
const getItemLayout = (data: any, index: number) => ({
length: LEFT_ITEM_HEIGHT,
offset: LEFT_ITEM_HEIGHT * index,
index
});
return ( return (
<SafeAreaView style={styles.safeAreaContainer}> <SafeAreaView style={styles.safeAreaContainer}>
<View style={styles.container}> <View style={styles.container}>
@@ -144,6 +176,7 @@ export default function ActivityScreen() {
{/* 左侧分类 */} {/* 左侧分类 */}
<View style={styles.leftWrapper}> <View style={styles.leftWrapper}>
<FlatList <FlatList
ref={leftListRef}
data={categoryList} data={categoryList}
renderItem={({ item }) => ( renderItem={({ item }) => (
<LeftItem <LeftItem
@@ -156,11 +189,11 @@ export default function ActivityScreen() {
)} )}
keyExtractor={item => item.id} keyExtractor={item => item.id}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
getItemLayout={getItemLayout} // 提高滚动性能
/> />
</View> </View>
{/* 右侧内容 */} {/* 右侧内容 */}
{/* 【优化点 1】给容器添加 onLayout 来获取高度 */}
<View style={styles.contentRight} onLayout={onRightListLayout}> <View style={styles.contentRight} onLayout={onRightListLayout}>
{/* 只有在获取到高度后才渲染 FlatList确保 minHeight 初始值正确 */} {/* 只有在获取到高度后才渲染 FlatList确保 minHeight 初始值正确 */}
{rightListHeight > 0 && ( {rightListHeight > 0 && (
@@ -172,15 +205,14 @@ export default function ActivityScreen() {
)} )}
keyExtractor={item => item.id} keyExtractor={item => item.id}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
// bounces 必须为 true (默认值)
bounces={true} bounces={true}
scrollEventThrottle={16} scrollEventThrottle={16}
onScroll={handleScroll} onScroll={handleScroll}
// 【优化点 2】为 iOS 添加,使其在内容不足时也能反弹 // 为 iOS 添加,使其在内容不足时也能反弹
alwaysBounceVertical={true} alwaysBounceVertical={true}
// 【优化点 3】设置 contentContainerStyle // 设置 contentContainerStyle
contentContainerStyle={[ contentContainerStyle={[
styles.rightListContent, styles.rightListContent,
{ {
@@ -208,8 +240,13 @@ const styles = StyleSheet.create({
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: '#e0e0e0', borderRightColor: '#e0e0e0',
}, },
// 一个容器严格控制Item高度
leftItemContainer: {
height: LEFT_ITEM_HEIGHT, // 严格高度
width: '100%',
},
leftItem: { leftItem: {
paddingVertical: 18, flex: 1, // 填满父容器
paddingHorizontal: 8, paddingHorizontal: 8,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -223,7 +260,7 @@ const styles = StyleSheet.create({
position: 'absolute', position: 'absolute',
left: 0, left: 0,
top: '50%', top: '50%',
marginTop: -12, marginTop: -12, // (24 / 2)
width: 3, width: 3,
height: 24, height: 24,
backgroundColor: '#1890ff', backgroundColor: '#1890ff',