Framer Motion 教程 - React 的生产级动画库
项目概述
Framer Motion 是一个为 React 设计的生产级动画库,它提供了简洁的声明式 API,使开发者能够轻松创建流畅、高性能的动画效果。Framer Motion 不仅支持基本的动画效果,还支持手势、布局动画和 SVG 动画等高级功能。
GitHub Stars:22k+
适用环境:React 项目
核心特点
声明式动画:使用简洁的声明式 API 创建动画
手势支持:支持点击、拖拽、悬停等手势
布局动画:自动处理布局变化的动画
SVG 动画:支持 SVG 元素的动画
性能优化:使用硬件加速和其他优化技术
Spring 物理:使用弹簧物理模型创建自然的动画
关键帧动画:支持关键帧动画
可组合性:动画可以轻松组合和嵌套
TypeScript 支持:完全支持 TypeScript
React 集成:与 React 深度集成,支持 Hooks
安装设置
1. 安装
在 React 项目中安装 Framer Motion:
# 使用 npm
npm install framer-motion
# 使用 yarn
yarn add framer-motion
# 使用 pnpm
pnpm add framer-motion2. 基本导入
在组件中导入 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 动画
使用 whileHover 和 whileTap 创建交互动画:
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. 页面过渡动画
场景:创建页面之间的平滑过渡动画
实现步骤:
- 安装 Framer Motion:
npm install framer-motion- 创建页面过渡组件:
// 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;- 在路由中使用:
// 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. 导航栏动画
场景:创建导航栏的滚动动画
实现步骤:
- 安装 Framer Motion:
npm install framer-motion- 创建导航栏组件:
// 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. 卡片悬停动画
场景:创建卡片的悬停动画效果
实现步骤:
- 安装 Framer Motion:
npm install framer-motion- 创建卡片组件:
// 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;- 在页面中使用:
// 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. 加载动画
场景:创建加载动画
实现步骤:
- 安装 Framer Motion:
npm install framer-motion- 创建加载组件:
// 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;- 在页面中使用:
// 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;最佳实践
使用硬件加速:优先使用
transform和opacity属性,它们可以使用硬件加速避免布局抖动:避免在动画中修改会导致布局抖动的属性
使用适当的动画类型:根据场景选择适当的动画类型,如弹簧动画或关键帧动画
优化性能:对于复杂的动画,考虑使用
willChange或其他优化技术使用变体:使用变体创建可重用的动画,提高代码可维护性
合理使用手势:只在必要时使用手势,避免过度使用
测试不同设备:在不同设备上测试动画,确保性能良好
使用 TypeScript:使用 TypeScript 提高代码质量和可维护性
文档化:为复杂的动画添加注释,说明动画的目的和工作原理
学习曲线:Framer Motion 有一定的学习曲线,建议先从简单的动画开始,逐步学习高级功能
常见问题与解决方案
1. 性能问题
问题:动画不流畅,出现卡顿
解决方案:
- 优先使用
transform和opacity属性 - 避免在动画中修改会导致布局抖动的属性
- 使用
willChange属性提示浏览器 - 考虑使用
useInView或其他技术只在必要时触发动画 - 对于复杂的动画,考虑使用
AnimatePresence的mode="wait"选项
2. 布局动画问题
问题:布局动画不工作或表现异常
解决方案:
- 确保使用
layout属性 - 对于条件渲染的元素,使用
AnimatePresence - 确保元素有明确的尺寸和位置
- 对于复杂的布局变化,考虑使用
layoutId
3. 手势问题
问题:手势不工作或表现异常
解决方案:
- 确保元素有明确的尺寸
- 对于拖拽,设置适当的
dragConstraints - 考虑使用
dragElastic和dragMomentum调整拖拽行为 - 对于复杂的手势,考虑使用
useGestureHook
4. 动画顺序问题
问题:动画顺序不正确
解决方案:
- 使用
delay属性控制动画顺序 - 使用
useAnimationHook 控制动画序列 - 对于复杂的动画序列,考虑使用
async/await
5. 与其他库的冲突
问题:与其他库(如 styled-components)冲突
解决方案:
- 确保正确导入和使用库
- 对于 styled-components,使用
motion作为基础组件 - 考虑使用
as属性指定渲染的元素类型
参考资源
GitHub 仓库:https://github.com/framer/motion
Framer 大学:https://www.framer.com/learn/
教程资源:
- Framer Motion 官方教程: https://www.framer.com/docs/motion/introduction/
- Framer Motion 示例: https://www.framer.com/motion/examples/
- Framer Motion 与 React 集成: https://www.framer.com/docs/motion/react/
总结
Framer Motion 是一个功能强大、易于使用的 React 动画库,它提供了简洁的声明式 API,使开发者能够轻松创建流畅、高性能的动画效果。通过本文的介绍,你应该已经了解了 Framer Motion 的核心功能、使用方法和最佳实践。
Framer Motion 的优势在于:
简洁的 API:使用简洁的声明式 API 创建动画,减少代码量
丰富的功能:支持手势、布局动画、SVG 动画等高级功能
性能优化:使用硬件加速和其他优化技术,确保动画流畅
与 React 集成:与 React 深度集成,支持 Hooks 和其他 React 特性
可扩展性:动画可以轻松组合和嵌套,适应复杂的场景
Framer Motion 适合各种 React 项目,无论是简单的页面过渡动画还是复杂的交互式动画,都可以通过 Framer Motion 轻松实现。它不仅可以提高用户体验,还可以使界面更加生动和吸引人。
如果你还没有在 React 项目中使用 Framer Motion,建议你尽快尝试,相信它会给你的开发工作带来很大的帮助。