You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 08:59:17