Framer Motion 教程 - React 的生产级动画库

项目概述

Framer Motion 是一个为 React 设计的生产级动画库,它提供了简洁的声明式 API,使开发者能够轻松创建流畅、高性能的动画效果。Framer Motion 不仅支持基本的动画效果,还支持手势、布局动画和 SVG 动画等高级功能。

核心特点

  1. 声明式动画:使用简洁的声明式 API 创建动画

  2. 手势支持:支持点击、拖拽、悬停等手势

  3. 布局动画:自动处理布局变化的动画

  4. SVG 动画:支持 SVG 元素的动画

  5. 性能优化:使用硬件加速和其他优化技术

  6. Spring 物理:使用弹簧物理模型创建自然的动画

  7. 关键帧动画:支持关键帧动画

  8. 可组合性:动画可以轻松组合和嵌套

  9. TypeScript 支持:完全支持 TypeScript

  10. React 集成:与 React 深度集成,支持 Hooks

安装设置

1. 安装

在 React 项目中安装 Framer Motion:

# 使用 npm
npm install framer-motion

# 使用 yarn
yarn add framer-motion

# 使用 pnpm
pnpm add framer-motion

2. 基本导入

在组件中导入 Framer Motion:

import { motion } from 'framer-motion';

基本使用

1. 基本动画

使用 motion 组件创建基本动画:

import { motion } from 'framer-motion';

const BasicAnimation = () => {
  return (
    <motion.div
      initial={{ opacity: 0, y: 50 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      style={{
        width: 100,
        height: 100,
        backgroundColor: 'blue',
        borderRadius: 8
      }}
    />
  );
};

export default BasicAnimation;

2. 状态驱动动画

使用状态驱动动画:

import { useState } from 'react';
import { motion } from 'framer-motion';

const StateDrivenAnimation = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? 'Close' : 'Open'}
      </button>
      <motion.div
        initial={{ height: 0, opacity: 0 }}
        animate={{ height: isOpen ? 200 : 0, opacity: isOpen ? 1 : 0 }}
        transition={{ duration: 0.3 }}
        style={{
          overflow: 'hidden',
          backgroundColor: 'lightgray',
          marginTop: 10,
          padding: isOpen ? 10 : 0
        }}
      >
        <p>Hello, this is a collapsible section!</p>
      </motion.div>
    </div>
  );
};

export default StateDrivenAnimation;

3. hover 和 tap 动画

使用 whileHoverwhileTap 创建交互动画:

import { motion } from 'framer-motion';

const InteractiveAnimation = () => {
  return (
    <motion.button
      whileHover={{ scale: 1.1, backgroundColor: '#3498db' }}
      whileTap={{ scale: 0.95 }}
      style={{
        padding: '10px 20px',
        borderRadius: 4,
        border: 'none',
        backgroundColor: '#2980b9',
        color: 'white',
        fontSize: 16,
        cursor: 'pointer'
      }}
    >
      Click me!
    </motion.button>
  );
};

export default InteractiveAnimation;

高级功能

1. 布局动画

使用 layout 属性创建布局动画:

import { useState } from 'react';
import { motion } from 'framer-motion';

const LayoutAnimation = () => {
  const [toggle, setToggle] = useState(false);

  return (
    <div style={{ display: 'flex', gap: 10, marginTop: 20 }}>
      <motion.div
        layout
        style={{
          width: toggle ? 200 : 100,
          height: 100,
          backgroundColor: 'blue',
          borderRadius: 8
        }}
      />
      <motion.div
        layout
        style={{
          width: toggle ? 100 : 200,
          height: 100,
          backgroundColor: 'red',
          borderRadius: 8
        }}
      />
      <button onClick={() => setToggle(!toggle)}>
        Toggle
      </button>
    </div>
  );
};

export default LayoutAnimation;

2. 弹簧物理

使用弹簧物理模型创建自然的动画:

import { motion } from 'framer-motion';

const SpringAnimation = () => {
  return (
    <motion.div
      initial={{ scale: 0 }}
      animate={{ scale: 1 }}
      transition={{
        type: 'spring',
        stiffness: 100,
        damping: 10
      }}
      style={{
        width: 100,
        height: 100,
        backgroundColor: 'green',
        borderRadius: 8
      }}
    />
  );
};

export default SpringAnimation;

3. 关键帧动画

使用关键帧创建复杂的动画:

import { motion } from 'framer-motion';

const KeyframesAnimation = () => {
  return (
    <motion.div
      animate={{
        x: [0, 100, 0],
        y: [0, 50, 0],
        rotate: [0, 360, 0],
        backgroundColor: ['blue', 'red', 'blue']
      }}
      transition={{
        duration: 5,
        ease: 'easeInOut',
        repeat: Infinity,
        repeatType: 'reverse'
      }}
      style={{
        width: 100,
        height: 100,
        borderRadius: 8
      }}
    />
  );
};

export default KeyframesAnimation;

4. 手势支持

使用手势创建交互动画:

import { motion } from 'framer-motion';

const GestureAnimation = () => {
  return (
    <motion.div
      drag
      dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
      whileTap={{ scale: 1.1 }}
      style={{
        width: 100,
        height: 100,
        backgroundColor: 'purple',
        borderRadius: 8,
        cursor: 'grab'
      }}
    />
  );
};

export default GestureAnimation;

5. SVG 动画

使用 Framer Motion 动画 SVG 元素:

import { motion } from 'framer-motion';

const SvgAnimation = () => {
  return (
    <svg width="200" height="200" viewBox="0 0 200 200">
      <motion.circle
        cx="100"
        cy="100"
        r="50"
        fill="none"
        stroke="blue"
        strokeWidth="2"
        initial={{ pathLength: 0 }}
        animate={{ pathLength: 1 }}
        transition={{ duration: 2, repeat: Infinity, repeatType: 'reverse' }}
      />
      <motion.circle
        cx="100"
        cy="100"
        r="30"
        fill="blue"
        initial={{ scale: 0 }}
        animate={{ scale: 1 }}
        transition={{ duration: 1, delay: 0.5 }}
      />
    </svg>
  );
};

export default SvgAnimation;

6. 动画变体

使用变体创建可重用的动画:

import { motion } from 'framer-motion';

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      staggerChildren: 0.1
    }
  }
};

const itemVariants = {
  hidden: { y: 20, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: {
      type: 'spring',
      stiffness: 100
    }
  }
};

const VariantsAnimation = () => {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      style={{ display: 'flex', gap: 10, marginTop: 20 }}
    >
      {[1, 2, 3, 4, 5].map((item) => (
        <motion.div
          key={item}
          variants={itemVariants}
          style={{
            width: 50,
            height: 50,
            backgroundColor: 'orange',
            borderRadius: 4
          }}
        />
      ))}
    </motion.div>
  );
};

export default VariantsAnimation;

7. useAnimation Hook

使用 useAnimation Hook 控制动画:

import { useState } from 'react';
import { motion, useAnimation } from 'framer-motion';

const UseAnimationHook = () => {
  const controls = useAnimation();
  const [isAnimating, setIsAnimating] = useState(false);

  const startAnimation = async () => {
    setIsAnimating(true);
    await controls.start({
      x: 100,
      rotate: 360,
      transition: { duration: 1 }
    });
    await controls.start({
      x: 0,
      rotate: 0,
      transition: { duration: 1 }
    });
    setIsAnimating(false);
  };

  return (
    <div>
      <motion.div
        animate={controls}
        style={{
          width: 100,
          height: 100,
          backgroundColor: 'pink',
          borderRadius: 8
        }}
      />
      <button
        onClick={startAnimation}
        disabled={isAnimating}
        style={{
          marginTop: 20,
          padding: '10px 20px',
          borderRadius: 4,
          border: 'none',
          backgroundColor: '#2980b9',
          color: 'white',
          cursor: isAnimating ? 'not-allowed' : 'pointer'
        }}
      >
        {isAnimating ? 'Animating...' : 'Start Animation'}
      </button>
    </div>
  );
};

export default UseAnimationHook;

实际应用场景

1. 页面过渡动画

场景:创建页面之间的平滑过渡动画

实现步骤

  1. 安装 Framer Motion:
npm install framer-motion
  1. 创建页面过渡组件:
// components/PageTransition.tsx
import { motion } from 'framer-motion';

interface PageTransitionProps {
  children: React.ReactNode;
}

const pageVariants = {
  initial: { opacity: 0, y: 20 },
  animate: { opacity: 1, y: 0 },
  exit: { opacity: 0, y: -20 }
};

const PageTransition: React.FC<PageTransitionProps> = ({ children }) => {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  );
};

export default PageTransition;
  1. 在路由中使用:
// App.tsx
import { Routes, Route, useLocation } from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import PageTransition from './components/PageTransition';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';

const App = () => {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <Routes location={location} key={location.pathname}>
        <Route
          path="/"
          element={
            <PageTransition>
              <HomePage />
            </PageTransition>
          }
        />
        <Route
          path="/about"
          element={
            <PageTransition>
              <AboutPage />
            </PageTransition>
          }
        />
      </Routes>
    </AnimatePresence>
  );
};

export default App;

2. 导航栏动画

场景:创建导航栏的滚动动画

实现步骤

  1. 安装 Framer Motion:
npm install framer-motion
  1. 创建导航栏组件:
// components/Navbar.tsx
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';

const Navbar = () => {
  const [scrolled, setScrolled] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      setScrolled(window.scrollY > 50);
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return (
    <motion.nav
      initial={{ y: -100 }}
      animate={{ y: 0 }}
      transition={{ duration: 0.5 }}
      style={{
        position: 'fixed',
        top: 0,
        left: 0,
        right: 0,
        backgroundColor: scrolled ? 'white' : 'transparent',
        boxShadow: scrolled ? '0 2px 10px rgba(0, 0, 0, 0.1)' : 'none',
        padding: '1rem',
        zIndex: 1000
      }}
    >
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <motion.div
          whileHover={{ scale: 1.05 }}
          style={{ fontSize: '1.5rem', fontWeight: 'bold', color: scrolled ? 'black' : 'white' }}
        >
          Logo
        </motion.div>
        <div style={{ display: 'flex', gap: '2rem' }}>
          {['Home', 'About', 'Services', 'Contact'].map((item) => (
            <motion.a
              key={item}
              href={`#${item.toLowerCase()}`}
              whileHover={{ scale: 1.1 }}
              whileTap={{ scale: 0.95 }}
              style={{ color: scrolled ? 'black' : 'white', textDecoration: 'none' }}
            >
              {item}
            </motion.a>
          ))}
        </div>
      </div>
    </motion.nav>
  );
};

export default Navbar;

3. 卡片悬停动画

场景:创建卡片的悬停动画效果

实现步骤

  1. 安装 Framer Motion:
npm install framer-motion
  1. 创建卡片组件:
// components/Card.tsx
import { motion } from 'framer-motion';

interface CardProps {
  title: string;
description: string;
  image: string;
}

const Card: React.FC<CardProps> = ({ title, description, image }) => {
  return (
    <motion.div
      whileHover={{
        y: -10,
        boxShadow: '0 10px 30px rgba(0, 0, 0, 0.1)'
      }}
      transition={{ duration: 0.3 }}
      style={{
        backgroundColor: 'white',
        borderRadius: 8,
        overflow: 'hidden',
        width: 300,
        boxShadow: '0 4px 10px rgba(0, 0, 0, 0.05)'
      }}
    >
      <div style={{ height: 200, overflow: 'hidden' }}>
        <motion.img
          src={image}
          alt={title}
          whileHover={{ scale: 1.1 }}
          transition={{ duration: 0.5 }}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      </div>
      <div style={{ padding: 16 }}>
        <motion.h3
          whileHover={{ color: '#3498db' }}
          style={{ margin: 0, marginBottom: 8 }}
        >
          {title}
        </motion.h3>
        <p style={{ margin: 0, color: '#666' }}>{description}</p>
      </div>
    </motion.div>
  );
};

export default Card;
  1. 在页面中使用:
// pages/HomePage.tsx
import Card from '../components/Card';

const HomePage = () => {
  const cards = [
    {
      title: 'Card 1',
description: 'This is a sample card description',
      image: 'https://picsum.photos/300/200'
    },
    {
      title: 'Card 2',
description: 'This is another sample card description',
      image: 'https://picsum.photos/300/201'
    },
    {
      title: 'Card 3',
description: 'This is a third sample card description',
      image: 'https://picsum.photos/300/202'
    }
  ];

  return (
    <div style={{ padding: 20, marginTop: 80 }}>
      <h1>Welcome to Home Page</h1>
      <div style={{ display: 'flex', gap: 20, marginTop: 40 }}>
        {cards.map((card, index) => (
          <Card
            key={index}
            title={card.title}
            description={card.description}
            image={card.image}
          />
        ))}
      </div>
    </div>
  );
};

export default HomePage;

4. 加载动画

场景:创建加载动画

实现步骤

  1. 安装 Framer Motion:
npm install framer-motion
  1. 创建加载组件:
// components/Loader.tsx
import { motion } from 'framer-motion';

const Loader = () => {
  return (
    <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 200 }}>
      <div style={{ display: 'flex', gap: 10 }}>
        {[1, 2, 3].map((item) => (
          <motion.div
            key={item}
            animate={{
              y: [0, -10, 0]
            }}
            transition={{
              duration: 0.5,
              repeat: Infinity,
              repeatType: 'loop',
              delay: item * 0.1
            }}
            style={{
              width: 15,
              height: 40,
              backgroundColor: '#3498db',
              borderRadius: 8
            }}
          />
        ))}
      </div>
    </div>
  );
};

export default Loader;
  1. 在页面中使用:
// pages/AboutPage.tsx
import { useState, useEffect } from 'react';
import Loader from '../components/Loader';

const AboutPage = () => {
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsLoading(false);
    }, 2000);

    return () => clearTimeout(timer);
  }, []);

  if (isLoading) {
    return <Loader />;
  }

  return (
    <div style={{ padding: 20, marginTop: 80 }}>
      <h1>About Page</h1>
      <p>This is the about page content. It loaded after the loading animation.</p>
    </div>
  );
};

export default AboutPage;

最佳实践

  1. 使用硬件加速:优先使用 transformopacity 属性,它们可以使用硬件加速

  2. 避免布局抖动:避免在动画中修改会导致布局抖动的属性

  3. 使用适当的动画类型:根据场景选择适当的动画类型,如弹簧动画或关键帧动画

  4. 优化性能:对于复杂的动画,考虑使用 willChange 或其他优化技术

  5. 使用变体:使用变体创建可重用的动画,提高代码可维护性

  6. 合理使用手势:只在必要时使用手势,避免过度使用

  7. 测试不同设备:在不同设备上测试动画,确保性能良好

  8. 使用 TypeScript:使用 TypeScript 提高代码质量和可维护性

  9. 文档化:为复杂的动画添加注释,说明动画的目的和工作原理

  10. 学习曲线:Framer Motion 有一定的学习曲线,建议先从简单的动画开始,逐步学习高级功能

常见问题与解决方案

1. 性能问题

问题:动画不流畅,出现卡顿

解决方案

  • 优先使用 transformopacity 属性
  • 避免在动画中修改会导致布局抖动的属性
  • 使用 willChange 属性提示浏览器
  • 考虑使用 useInView 或其他技术只在必要时触发动画
  • 对于复杂的动画,考虑使用 AnimatePresencemode=&quot;wait&quot; 选项

2. 布局动画问题

问题:布局动画不工作或表现异常

解决方案

  • 确保使用 layout 属性
  • 对于条件渲染的元素,使用 AnimatePresence
  • 确保元素有明确的尺寸和位置
  • 对于复杂的布局变化,考虑使用 layoutId

3. 手势问题

问题:手势不工作或表现异常

解决方案

  • 确保元素有明确的尺寸
  • 对于拖拽,设置适当的 dragConstraints
  • 考虑使用 dragElasticdragMomentum 调整拖拽行为
  • 对于复杂的手势,考虑使用 useGesture Hook

4. 动画顺序问题

问题:动画顺序不正确

解决方案

  • 使用 delay 属性控制动画顺序
  • 使用 useAnimation Hook 控制动画序列
  • 对于复杂的动画序列,考虑使用 async/await

5. 与其他库的冲突

问题:与其他库(如 styled-components)冲突

解决方案

  • 确保正确导入和使用库
  • 对于 styled-components,使用 motion 作为基础组件
  • 考虑使用 as 属性指定渲染的元素类型

参考资源

总结

Framer Motion 是一个功能强大、易于使用的 React 动画库,它提供了简洁的声明式 API,使开发者能够轻松创建流畅、高性能的动画效果。通过本文的介绍,你应该已经了解了 Framer Motion 的核心功能、使用方法和最佳实践。

Framer Motion 的优势在于:

  1. 简洁的 API:使用简洁的声明式 API 创建动画,减少代码量

  2. 丰富的功能:支持手势、布局动画、SVG 动画等高级功能

  3. 性能优化:使用硬件加速和其他优化技术,确保动画流畅

  4. 与 React 集成:与 React 深度集成,支持 Hooks 和其他 React 特性

  5. 可扩展性:动画可以轻松组合和嵌套,适应复杂的场景

Framer Motion 适合各种 React 项目,无论是简单的页面过渡动画还是复杂的交互式动画,都可以通过 Framer Motion 轻松实现。它不仅可以提高用户体验,还可以使界面更加生动和吸引人。

如果你还没有在 React 项目中使用 Framer Motion,建议你尽快尝试,相信它会给你的开发工作带来很大的帮助。

« 上一篇 TypeScript 教程 - JavaScript 的超集 下一篇 » GSAP 教程 - 高性能 JavaScript 动画库