优化动画切换效果
This commit is contained in:
@@ -10,11 +10,24 @@ import {
|
|||||||
Image,
|
Image,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
LayoutChangeEvent
|
LayoutChangeEvent,
|
||||||
|
LayoutAnimation, // 动画
|
||||||
|
UIManager, // 动画
|
||||||
|
Platform // 动画
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
|
||||||
const screenWidth = Dimensions.get('window').width;
|
const screenWidth = Dimensions.get('window').width;
|
||||||
|
|
||||||
|
// 为 Android 启用 LayoutAnimation (必须的样板代码)
|
||||||
|
if (
|
||||||
|
Platform.OS === 'android' &&
|
||||||
|
UIManager.setLayoutAnimationEnabledExperimental
|
||||||
|
) {
|
||||||
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 数据模型和常量 ---
|
||||||
|
|
||||||
// 分类列表
|
// 分类列表
|
||||||
const categoryList = [
|
const categoryList = [
|
||||||
{ id: '0', title: '捕鱼', icon: '🎣' },
|
{ id: '0', title: '捕鱼', icon: '🎣' },
|
||||||
@@ -33,8 +46,8 @@ const categoryList = [
|
|||||||
|
|
||||||
// 随机生成游戏数据
|
// 随机生成游戏数据
|
||||||
const generateRandomGames = (categoryId: string, baseName: string) => {
|
const generateRandomGames = (categoryId: string, baseName: string) => {
|
||||||
// 调整随机数,使其更容易出现空列表或少量item,以测试优化效果
|
// 产生 0 到 5 个 Item,用于测试空白区域滚动
|
||||||
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'];
|
||||||
|
|
||||||
@@ -55,53 +68,45 @@ categoryList.forEach(cat => {
|
|||||||
mockData[cat.id] = generateRandomGames(cat.id, cat.title);
|
mockData[cat.id] = generateRandomGames(cat.id, cat.title);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定义左侧Item的固定高度
|
// 定义左侧Item的固定高度,用于 getItemLayout
|
||||||
// 样式中: paddingVertical(18*2) + icon(24) + iconMargin(4) + title(12) 估算
|
|
||||||
// 我们在样式中直接指定为 76
|
|
||||||
const LEFT_ITEM_HEIGHT = 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);
|
||||||
|
|
||||||
// 创建左侧列表的ref
|
|
||||||
const leftListRef = useRef<FlatList>(null);
|
const leftListRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
// State,用于存储右侧列表容器的实际高度
|
|
||||||
const [rightListHeight, setRightListHeight] = useState(0);
|
const [rightListHeight, setRightListHeight] = useState(0);
|
||||||
|
|
||||||
// Slogan:当selectedId变化时,自动滚动左侧列表
|
// 优化 1: 左侧列表自动滚动,确保选中项可见
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 1. 找到当前 selectedId 对应的索引
|
|
||||||
const newIndex = categoryList.findIndex(item => item.id === selectedId);
|
const newIndex = categoryList.findIndex(item => item.id === selectedId);
|
||||||
|
|
||||||
// 2. 如果找到了 (index > -1) 并且 ref 已经准备好
|
|
||||||
if (leftListRef.current && newIndex > -1) {
|
if (leftListRef.current && newIndex > -1) {
|
||||||
// 3. 命令 FlatList 滚动到该索引
|
|
||||||
leftListRef.current.scrollToIndex({
|
leftListRef.current.scrollToIndex({
|
||||||
index: newIndex,
|
index: newIndex,
|
||||||
animated: true,
|
animated: true,
|
||||||
// viewPosition: 0.5 会将item滚动到列表的中间位置,体验更好
|
viewPosition: 0.5 // 滚动到居中位置
|
||||||
viewPosition: 0.5
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [selectedId]); // 依赖项是 selectedId,它变化时此Slogan执行
|
}, [selectedId]);
|
||||||
|
|
||||||
|
|
||||||
// 支持循环切换分类
|
// 处理分类切换的核心逻辑
|
||||||
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);
|
||||||
const total = categoryList.length;
|
const total = categoryList.length;
|
||||||
let newIndex = direction === 'next' ? (currentIndex + 1) % total : (currentIndex - 1 + total) % total;
|
let newIndex = direction === 'next' ? (currentIndex + 1) % total : (currentIndex - 1 + total) % total;
|
||||||
|
|
||||||
|
// 优化 3: 在 state 改变之前,配置下一次布局动画
|
||||||
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||||
|
|
||||||
// 切换分类
|
// 切换分类
|
||||||
setSelectedId(categoryList[newIndex].id);
|
setSelectedId(categoryList[newIndex].id);
|
||||||
// 切换时重置滚动
|
|
||||||
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
rightListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
// 实时检测滚动边界 + 循环切换
|
// 优化 2: 实时检测滚动边界 + 循环切换 (处理空白区域滚动)
|
||||||
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
const { contentOffset, contentSize, layoutMeasurement } = e.nativeEvent;
|
||||||
const y = contentOffset.y;
|
const y = contentOffset.y;
|
||||||
@@ -117,8 +122,7 @@ export default function ActivityScreen() {
|
|||||||
setTimeout(() => setIsSwitching(false), 600);
|
setTimeout(() => setIsSwitching(false), 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 滑到底部并继续上推
|
// 滑到底部并继续上推 (包含内容不足以填满屏幕的情况)
|
||||||
// contentHeight <= layoutHeight 检查内容是否填满
|
|
||||||
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');
|
||||||
@@ -126,7 +130,7 @@ export default function ActivityScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// onLayout 事件处理函数
|
// 获取右侧列表容器的高度 (用于 minHeight 优化)
|
||||||
const onRightListLayout = (event: LayoutChangeEvent) => {
|
const onRightListLayout = (event: LayoutChangeEvent) => {
|
||||||
const { height } = event.nativeEvent.layout;
|
const { height } = event.nativeEvent.layout;
|
||||||
setRightListHeight(height);
|
setRightListHeight(height);
|
||||||
@@ -135,7 +139,6 @@ export default function ActivityScreen() {
|
|||||||
|
|
||||||
// 左侧Item组件
|
// 左侧Item组件
|
||||||
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
|
const LeftItem = ({ id, title, icon, isSelected, onPress }: any) => (
|
||||||
// 包裹在 View 中,并精确设置高度,确保 getItemLayout 的计算正确
|
|
||||||
<View style={styles.leftItemContainer}>
|
<View style={styles.leftItemContainer}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.leftItem, isSelected && styles.leftItemActive]}
|
style={[styles.leftItem, isSelected && styles.leftItemActive]}
|
||||||
@@ -184,7 +187,11 @@ export default function ActivityScreen() {
|
|||||||
title={item.title}
|
title={item.title}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
isSelected={selectedId === item.id}
|
isSelected={selectedId === item.id}
|
||||||
onPress={() => setSelectedId(item.id)}
|
onPress={() => {
|
||||||
|
// 优化 3: 左侧点击时也触发动画
|
||||||
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||||
|
setSelectedId(item.id);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
@@ -195,7 +202,7 @@ export default function ActivityScreen() {
|
|||||||
|
|
||||||
{/* 右侧内容 */}
|
{/* 右侧内容 */}
|
||||||
<View style={styles.contentRight} onLayout={onRightListLayout}>
|
<View style={styles.contentRight} onLayout={onRightListLayout}>
|
||||||
{/* 只有在获取到高度后才渲染 FlatList,确保 minHeight 初始值正确 */}
|
{/* 只有在获取到高度后才渲染 FlatList */}
|
||||||
{rightListHeight > 0 && (
|
{rightListHeight > 0 && (
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={rightListRef}
|
ref={rightListRef}
|
||||||
@@ -208,16 +215,11 @@ export default function ActivityScreen() {
|
|||||||
bounces={true}
|
bounces={true}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
alwaysBounceVertical={true} // 确保内容不足时也能滚动 (iOS)
|
||||||
// 为 iOS 添加,使其在内容不足时也能反弹
|
|
||||||
alwaysBounceVertical={true}
|
|
||||||
|
|
||||||
// 设置 contentContainerStyle
|
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.rightListContent,
|
styles.rightListContent,
|
||||||
{
|
{
|
||||||
// 确保内容容器的最小高度
|
// 优化 2: 确保内容容器的最小高度,实现空白区域滚动
|
||||||
// 始终比 FlatList 视图本身高 1 像素
|
|
||||||
minHeight: rightListHeight + 1
|
minHeight: rightListHeight + 1
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
@@ -240,9 +242,9 @@ const styles = StyleSheet.create({
|
|||||||
borderRightWidth: 1,
|
borderRightWidth: 1,
|
||||||
borderRightColor: '#e0e0e0',
|
borderRightColor: '#e0e0e0',
|
||||||
},
|
},
|
||||||
// 一个容器,严格控制Item高度
|
// 严格控制Item高度,用于 getItemLayout
|
||||||
leftItemContainer: {
|
leftItemContainer: {
|
||||||
height: LEFT_ITEM_HEIGHT, // 严格高度
|
height: LEFT_ITEM_HEIGHT,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
leftItem: {
|
leftItem: {
|
||||||
|
|||||||
Reference in New Issue
Block a user