React中使用Framer Motion实现商品卡片飞入购物车动画问题
商品卡片飞入购物车动画解决方案
问题根源
你当前用全局状态xValue、yValue控制所有卡片的动画,导致所有卡片共享同一动画参数,点击任意卡片都会集体移动到同一位置。解决核心是给每个卡片独立维护动画状态。
Framer Motion 实现方案
使用Framer Motion的useAnimation钩子,给每个卡片分配独立的动画控制器,避免全局状态冲突。
代码示例
import { motion, useAnimation } from "framer-motion"; import { useState } from "react"; function ProductCard({ product, order, set_order, cartRef }) { // 每个卡片专属的动画控制器 const controls = useAnimation(); const [isAnimating, setIsAnimating] = useState(false); const handleClick = async (p) => { if (isAnimating) return; // 动画中禁止重复点击 // 执行加入购物车逻辑 await set_order({ ...order, products: { ...order?.products, [order?.products?.length]: { ...p, qte: 1 }, }, }); // 获取卡片与购物车的位置,计算偏移量(替代固定x/y值) const cardRect = event.currentTarget.getBoundingClientRect(); const cartRect = cartRef.current.getBoundingClientRect(); const xOffset = cartRect.left - cardRect.left; const yOffset = cartRect.top - cardRect.top; // 触发飞入动画 setIsAnimating(true); await controls.start({ x: xOffset, y: yOffset, opacity: 0, transition: { duration: 1 } }); // 动画结束后重置卡片状态 setIsAnimating(false); controls.start({ x: 0, y: 0, opacity: 1, transition: { duration: 0 } }); }; return ( <motion.div initial={{ opacity: 0 }} animate={controls} style={{ position: "relative" }} > <Card onClick={() => handleClick(product)} disabled={isAnimating} > {/* 卡片内容 */} </Card> </motion.div> ); } // 购物车组件需添加ref function Cart() { const cartRef = useRef(null); return <div ref={cartRef}>购物车</div>; }
关键优化点
- 每个卡片独立使用
useAnimation,避免全局状态污染 - 通过DOM位置计算动态偏移量,适配不同屏幕尺寸
- 动画期间禁用卡片点击,防止重复触发
GSAP 实现方案
GSAP通过直接操作DOM元素样式实现动画,逻辑更直观,适合复杂动画场景。
代码示例
import gsap from "gsap"; import { useState, useRef } from "react"; function ProductCard({ product, order, set_order, cartRef }) { const cardRef = useRef(null); const [isAnimating, setIsAnimating] = useState(false); const handleClick = async (p) => { if (isAnimating) return; await set_order({ ...order, products: { ...order?.products, [order?.products?.length]: { ...p, qte: 1 }, }, }); // 计算位置偏移 const cardRect = cardRef.current.getBoundingClientRect(); const cartRect = cartRef.current.getBoundingClientRect(); const xOffset = cartRect.left - cardRect.left; const yOffset = cartRect.top - cardRect.top; // 执行飞入动画 setIsAnimating(true); gsap.to(cardRef.current, { x: xOffset, y: yOffset, opacity: 0, duration: 1, onComplete: () => { // 重置卡片状态 gsap.set(cardRef.current, { x: 0, y: 0, opacity: 1 }); setIsAnimating(false); } }); }; return ( <div ref={cardRef} style={{ position: "relative" }}> <Card onClick={() => handleClick(product)} disabled={isAnimating} > {/* 卡片内容 */} </Card> </div> ); }
其他可选动画库
React Spring
基于弹簧物理的动画库,动画效果更自然:
import { useSpring, animated } from "react-spring"; import { useState } from "react"; function ProductCard({ product, order, set_order, cartRef }) { const [{ x, y, opacity }, setSpring] = useSpring(() => ({ x: 0, y: 0, opacity: 1 })); const [isAnimating, setIsAnimating] = useState(false); const handleClick = async (p) => { if (isAnimating) return; await set_order(/* 加入购物车逻辑 */); const cardRect = event.currentTarget.getBoundingClientRect(); const cartRect = cartRef.current.getBoundingClientRect(); const xOffset = cartRect.left - cardRect.left; const yOffset = cartRect.top - cardRect.top; setIsAnimating(true); setSpring({ x: xOffset, y: yOffset, opacity: 0, config: { duration: 1000 } }); setTimeout(() => { setSpring({ x: 0, y: 0, opacity: 1, config: { duration: 0 } }); setIsAnimating(false); }, 1000); }; return ( <animated.div style={{ x, y, opacity, position: "relative" }}> <Card onClick={() => handleClick(product)} disabled={isAnimating}> {/* 卡片内容 */} </Card> </animated.div> ); }
内容的提问来源于stack exchange,提问作者ayman jarmoune
相关产品推荐
相关产品推荐

