React Native实现类似Flutter的商品加购飞行动画求助
React Native 实现商品添加购物车的飞行动画方案
核心思路
要实现类似的「商品图片旋转飞入购物车」效果,核心是创建一个独立的动画层:点击添加按钮时,克隆当前商品的图片到动画层,通过React Native的Animated API组合平移、旋转、透明度动画,让图片从商品位置平滑过渡到购物车图标位置,动画结束后清理动画层并更新购物车状态。
分步实现代码示例
以下是完整的可运行示例代码,包含商品列表、底部TabBar和动画逻辑:
import React, { useRef, useState } from 'react'; import { View, FlatList, Image, Text, TouchableOpacity, Animated, UIManager, findNodeHandle, StatusBar, Platform } from 'react-native'; const ProductCartAnimation = () => { // 模拟商品数据 const productList = useRef([ { id: '1', name: '复古帆布包', image: 'https://via.placeholder.com/80' }, { id: '2', name: '简约马克杯', image: 'https://via.placeholder.com/80' }, { id: '3', name: '纯棉T恤', image: 'https://via.placeholder.com/80' }, ]).current; const [cartItemCount, setCartItemCount] = useState(0); // 动画相关变量 const animationProgress = useRef(new Animated.Value(0)).current; const isAnimationRunning = useRef(false); // 存储元素引用:商品图片、购物车图标、动画层图片 const productImageRefs = useRef({}); const cartIconRef = useRef(null); const animatedCloneImage = useRef(null); // 获取元素在屏幕上的坐标(含状态栏适配) const getElementScreenPosition = async (elementRef) => { return new Promise((resolve) => { UIManager.measureInWindow(findNodeHandle(elementRef), (x, y, width, height) => { // 安卓需要加上状态栏高度修正坐标 const adjustedY = Platform.OS === 'android' ? y + StatusBar.currentHeight : y; resolve({ x, y: adjustedY, width, height }); }); }); }; // 处理添加购物车点击事件 const handleAddToCart = async (productId) => { if (isAnimationRunning.current) return; // 防止重复触发动画 isAnimationRunning.current = true; // 获取商品图片和购物车图标的屏幕位置 const productPos = await getElementScreenPosition(productImageRefs.current[productId]); const cartPos = await getElementScreenPosition(cartIconRef.current); // 计算动画起始/结束位置(让图片中心对齐购物车中心) const startCenterX = productPos.x + productPos.width / 2; const startCenterY = productPos.y + productPos.height / 2; const endCenterX = cartPos.x + cartPos.width / 2; const endCenterY = cartPos.y + cartPos.height / 2; // 重置动画进度 animationProgress.setValue(0); // 定义动画插值样式 const animatedStyle = { transform: animationProgress.interpolate({ inputRange: [0, 1], outputRange: [ [ { translateX: startCenterX - productPos.width / 2 }, { translateY: startCenterY - productPos.height / 2 }, { rotate: '0deg' } ], [ { translateX: endCenterX - productPos.width / 2 }, { translateY: endCenterY - productPos.height / 2 }, { rotate: '360deg' } ] ] }), opacity: animationProgress.interpolate({ inputRange: [0, 0.9, 1], outputRange: [1, 1, 0] }), width: productPos.width, height: productPos.height, position: 'absolute', zIndex: 9999, borderRadius: 8 }; // 克隆当前商品图片到动画层 const targetProduct = productList.find(item => item.id === productId); animatedCloneImage.current = ( <Animated.Image source={{ uri: targetProduct.image }} style={animatedStyle} /> ); // 强制刷新渲染动画层 setCartItemCount(prev => prev); // 执行动画 Animated.timing(animationProgress, { toValue: 1, duration: 800, useNativeDriver: true, easing: Animated.easeOut }).start(() => { // 动画结束后清理 animatedCloneImage.current = null; isAnimationRunning.current = false; // 更新购物车数量 setCartItemCount(prev => prev + 1); }); }; return ( <View style={{ flex: 1, backgroundColor: '#f5f5f5' }}> {/* 商品列表 */} <FlatList data={productList} keyExtractor={item => item.id} contentContainerStyle={{ paddingTop: StatusBar.currentHeight }} renderItem={({ item }) => ( <View style={{ flexDirection: 'row', padding: 16, backgroundColor: '#fff', marginBottom: 8, alignItems: 'center' }}> <Image ref={ref => productImageRefs.current[item.id] = ref} source={{ uri: item.image }} style={{ width: 80, height: 80, borderRadius: 8 }} /> <View style={{ flex: 1, marginLeft: 12 }}> <Text style={{ fontSize: 16, fontWeight: '500', color: '#333' }}>{item.name}</Text> <TouchableOpacity style={{ marginTop: 10, backgroundColor: '#0066cc', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 4, alignSelf: 'flex-start' }} onPress={() => handleAddToCart(item.id)} > <Text style={{ color: '#fff', fontSize: 14 }}>加入购物车</Text> </TouchableOpacity> </View> </View> )} /> {/* 动画层:用于展示飞入购物车的商品图片 */} {animatedCloneImage.current} {/* 底部TabBar */} <View style={{ flexDirection: 'row', height: 60, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: '#eee', alignItems: 'center', justifyContent: 'space-around' }}> <TouchableOpacity style={{ alignItems: 'center' }}> <Text style={{ fontSize: 22 }}>🏠</Text> <Text style={{ fontSize: 12, marginTop: 2 }}>首页</Text> </TouchableOpacity> <TouchableOpacity ref={cartIconRef} style={{ alignItems: 'center' }}> <View style={{ position: 'relative' }}> <Text style={{ fontSize: 22 }}>🛒</Text> {cartItemCount > 0 && ( <View style={{ position: 'absolute', top: -6, right: -6, backgroundColor: '#ff3b30', borderRadius: 10, width: 20, height: 20, alignItems: 'center', justifyContent: 'center' }}> <Text style={{ color: '#fff', fontSize: 12, fontWeight: 'bold' }}>{cartItemCount}</Text> </View> )} </View> <Text style={{ fontSize: 12, marginTop: 2 }}>购物车</Text> </TouchableOpacity> <TouchableOpacity style={{ alignItems: 'center' }}> <Text style={{ fontSize: 22 }}>👤</Text> <Text style={{ fontSize: 12, marginTop: 2 }}>我的</Text> </TouchableOpacity> </View> </View> ); }; export default ProductCartAnimation;
关键细节与优化建议
- 防重复点击:通过
isAnimationRunning标记动画状态,避免短时间内多次点击触发多个动画。 - 坐标适配:安卓设备需要加上状态栏高度修正Y轴坐标,确保动画路径准确。
- 性能优化:开启
useNativeDriver: true让动画在原生线程执行,避免JS线程阻塞导致卡顿。 - 动画曲线:使用
Animated.easeOut让动画结束阶段减速,更符合自然的物理效果。 - 样式细节:动画层图片设置与商品图片一致的圆角,保持视觉统一;动画结束时通过透明度渐变隐藏图片,避免突兀消失。
内容的提问来源于stack exchange,提问作者Gokul Raj
相关产品推荐
相关产品推荐

