📚 现代前端开发 目录
阶段4:UI与体验
第 27 / 35 课

动画库:Framer Motion

React动画的艺术

📖 核心概念

💻 代码实现

Framer Motion动画 ✅ tsx
import { motion, AnimatePresence, useMotionValue, useTransform, useSpring } from 'framer-motion';

// 基础动画
function FadeIn({ children }: { children: ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0, y: -20 }}
      transition={{ duration: 0.3, ease: 'easeOut' }}
    >
      {children}
    </motion.div>
  );
}

// Variants - 编排动画
const container = {
  hidden: { opacity: 0 },
  show: {
    opacity: 1,
    transition: { staggerChildren: 0.1 }, // 子元素依次出现
  },
};

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
};

function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul variants={container} initial="hidden" animate="show">
      {items.map((text, i) => (
        <motion.li key={i} variants={item}>{text}</motion.li>
      ))}
    </motion.ul>
  );
}

// 交互手势
function DraggableCard() {
  const x = useMotionValue(0);
  const rotate = useTransform(x, [-200, 200], [-30, 30]);
  const opacity = useTransform(x, [-200, -100, 0, 100, 200], [0.5, 1, 1, 1, 0.5]);

  return (
    <motion.div
      drag="x"
      style={{ x, rotate, opacity }}
      dragConstraints={{ left: -200, right: 200 }}
      dragElastic={0.8}
      onDragEnd={(_, info) => {
        if (Math.abs(info.offset.x) > 100) {
          // 滑动超过阈值,执行操作
          console.log(info.offset.x > 0 ? '喜欢' : '不喜欢');
        }
      }}
    >
      左右滑动
    </motion.div>
  );
}

// AnimatePresence - 条件渲染动画
function TabContent({ tab }: { tab: string }) {
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={tab}
        initial={{ opacity: 0, x: 20 }}
        animate={{ opacity: 1, x: 0 }}
        exit={{ opacity: 0, x: -20 }}
        transition={{ duration: 0.2 }}
      >
        {tab === 'a' ? '内容A' : '内容B'}
      </motion.div>
    </AnimatePresence>
  );
}
布局动画与路由过渡 ✅ tsx
// 共享布局动画
import { motion, LayoutGroup } from 'framer-motion';

function ExpandableCard({ item }: { item: Item }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <motion.div
      layout
      onClick={() => setExpanded(!expanded)}
      style={{ borderRadius: 12, background: '#1e293b' }}
    >
      <motion.h3 layout="position">{item.title}</motion.h3>
      <AnimatePresence>
        {expanded && (
          <motion.p
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            {item.description}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  );
}

// 共享元素过渡
function Gallery() {
  const [selectedId, setSelectedId] = useState<string | null>(null);

  return (
    <LayoutGroup>
      {items.map(item => (
        <motion.div
          key={item.id}
          layoutId={item.id === selectedId ? 'selected' : undefined}
          onClick={() => setSelectedId(item.id)}
        >
          <img src={item.thumbnail} />
        </motion.div>
      ))}

      <AnimatePresence>
        {selectedId && (
          <motion.div
            layoutId="selected"
            className="fullscreen-overlay"
          >
            <img src={items.find(i => i.id === selectedId)!.full} />
            <button onClick={() => setSelectedId(null)}>关闭</button>
          </motion.div>
        )}
      </AnimatePresence>
    </LayoutGroup>
  );
}

// 路由过渡动画
function PageTransition({ children }: { children: ReactNode }) {
  return (
    <motion.div
      initial={{ clipPath: 'circle(0% at 50% 50%)' }}
      animate={{ clipPath: 'circle(150% at 50% 50%)' }}
      exit={{ clipPath: 'circle(0% at 50% 50%)' }}
      transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}
    >
      {children}
    </motion.div>
  );
}

// 滚动触发动画
function ScrollReveal() {
  return (
    <motion.div
      initial={{ opacity: 0, y: 50 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.6 }}
    >
      滚动到此处时出现
    </motion.div>
  );
}

🎯 练习任务

🏆 成就解锁
动画艺术家 — 掌握Framer Motion,创造流畅优雅的React动画