React 动画与 Framer Motion
动画方案概述
React 中实现动画有多种方案,从简单到复杂:
- CSS Transition/Animation:最简单的动画方案,适合简单的状态切换
- CSS-in-JS 动画:如
styled-components的keyframes - React Motion:早期的 React 动画库
- Framer Motion:目前最流行的 React 动画库,API 设计优雅
- React Spring:基于物理的动画库
选择依据:
- 简单状态切换 → CSS Transition
- 复杂交互动画 → Framer Motion
- 物理仿真动画 → React Spring
CSS 动画基础
CSS Transition
transition 适合简单的 A → B 状态变化:
function Button() {
const [isHovered, setIsHovered] = useState(false);
return (
<button
style={{
transition: 'all 0.3s ease',
transform: isHovered ? 'scale(1.05)' : 'scale(1)',
backgroundColor: isHovered ? '#0056b3' : '#007bff',
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Click me
</button>
);
}
CSS Keyframes
@keyframes 适合复杂的、时间线式的动画:
const keyframes = `
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
`;
function AnimatedComponent({ isVisible }) {
return (
<>
<style>{keyframes}</style>
<div style={{
animation: isVisible ? 'slideIn 0.5s ease' : 'fadeOut 0.3s ease'
}}>
Content
</div>
</>
);
}
Framer Motion 简介
Framer Motion 是由 Framer 公司开发的 React 动画库,它的核心概念是:
- Animate:任何可动画属性的值
- Gesture:点击、拖拽、悬停等交互
- Layout:自动布局动画
- MotionValue:可追踪的动画值
安装
npm install framer-motion
基础动画:motion 组件
import { motion } from 'framer-motion';
function AnimatedBox() {
return (
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
>
Content
</motion.div>
);
}
motion 组件的行为:
initial:组件挂载前的状态animate:组件挂载后或变量变化后的目标状态transition:如何从初始状态过渡到目标状态
动画变体(Variants)
Variants 让你把多个动画状态组合成一个「动画状态机」:
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1 // 子元素延迟
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
function StaggeredList() {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{[1, 2, 3].map(i => (
<motion.li key={i} variants={itemVariants}>
Item {i}
</motion.li>
))}
</motion.ul>
);
}
Variants 的优势:
- 代码更清晰
- 支持
staggerChildren实现交错动画 - 可以动态切换整个状态机
手势动画
Hover
function ScaleButton() {
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Click me
</motion.button>
);
}
拖拽(Drag)
function DraggableBox() {
return (
<motion.div
drag="x" // 只允许水平拖拽
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.1} // 弹性系数
whileDrag={{ scale: 1.1 }}
>
Drag me
</motion.div>
);
}
高级拖拽:拖拽释放到目标
function DropZone() {
return (
<motion.div
id="drop-zone"
style={{
width: 200,
height: 200,
backgroundColor: '#eee',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Drop here
</motion.div>
);
}
function DraggableItem() {
const { drag, setDrag } = useState();
return (
<motion.div
drag
dragSnapToOrigin
whileDrag={{ scale: 1.1 }}
onDragEnd={(e, info) => {
// 检查是否释放到目标区域
const dropTarget = document.getElementById('drop-zone');
const dropRect = dropTarget.getBoundingClientRect();
if (
info.point.x >= dropRect.left &&
info.point.x <= dropRect.right &&
info.point.y >= dropRect.top &&
info.point.y <= dropRect.bottom
) {
console.log('Dropped in zone!');
}
}}
>
Drag me
</motion.div>
);
}
Layout 动画
Layout 动画是 Framer Motion 最强大的特性之一——当元素位置或大小变化时,自动添加平滑过渡:
function LayoutDemo() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<motion.div
layout
style={{
width: isExpanded ? 400 : 200,
height: isExpanded ? 400 : 200,
backgroundColor: '#007bff',
borderRadius: 10
}}
onClick={() => setIsExpanded(!isExpanded)}
>
Click to resize
</motion.div>
);
}
当 layout prop 添加后,Framer Motion 会自动计算位置和大小的变化,并添加平滑过渡动画。
列表动画
function AnimatedList({ items, onRemove }) {
return (
<motion.ul layout>
{items.map(item => (
<motion.li
key={item.id}
layout
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: -100 }}
>
<span>{item.text}</span>
<button onClick={() => onRemove(item.id)}>Remove</button>
</motion.li>
))}
</motion.ul>
);
}
exit prop 定义元素被移除前的动画,配合 AnimatePresence 组件使用。
AnimatePresence
AnimatePresence 允许在组件卸载时执行退出动画:
import { AnimatePresence, motion } from 'framer-motion';
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div onClick={onClose}>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
>
Modal Content
</motion.div>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
关键点:组件必须作为 AnimatePresence 的子组件才能在卸载时执行 exit 动画。
Motion Values
useMotionValue 让你创建可追踪的动画值:
function MotionValueDemo() {
const x = useMotionValue(0);
return (
<motion.div
style={{ x }}
drag="x"
dragConstraints={{ left: 0, right: 300 }}
>
<MotionValueDisplay value={x} />
</motion.div>
);
}
function MotionValueDisplay({ value }) {
// useMotionValue 不触发 React 重新渲染
// 需要用 useMotionValueEvent 来监听变化
useMotionValueEvent(value, 'change', (latest) => {
console.log('x is now:', latest);
});
return null;
}
useTransform
useTransform 可以在一个 motion value 的变化范围内计算另一个值:
function ParallaxCard() {
const y = useMotionValue(0);
const rotate = useTransform(y, [0, 300], [0, 10]);
const opacity = useTransform(y, [0, 300], [1, 0.5]);
return (
<motion.div style={{ y, rotate, opacity }}>
Card
</motion.div>
);
}
动画性能优化
GPU 加速
Framer Motion 默认使用 transform 和 opacity 进行动画,这两个属性不会触发 layout 或 paint:
// 好:使用 transform
<motion.div animate={{ x: 100 }} />
// 差:使用宽高变化(触发 layout)
<motion.div animate={{ width: 200 }} />
避免动画时重新渲染
// 好:只动画不渲染
<motion.div animate={{ scale: 1.1 }} />
// 差:依赖变化触发重新渲染
const [scale, setScale] = useState(1);
<motion.div animate={{ scale }} onClick={() => setScale(1.1)} />
使用 layoutId 实现共享动画
layoutId 允许在不同组件之间实现「形变」动画:
function SharedLayoutDemo() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div>
<motion.div
layoutId="card"
style={{ backgroundColor: '#007bff' }}
onClick={() => setIsExpanded(!isExpanded)}
>
{!isExpanded && <div>Preview</div>}
{isExpanded && <div>Full Content</div>}
</motion.div>
</div>
);
}
当 layoutId 相同时,Framer Motion 会自动计算两个状态之间的形变并添加过渡动画。
面试中的表达
面试中聊到 React 动画,通常是在考察你对动画原理和用户体验的理解:
关于动画方案的选择,我的经验是:简单交互用 CSS Transition,复杂交互动画用 Framer Motion。Framer Motion 的核心优势是声明式 API 和 layout 动画——你只需要描述动画的初始状态和目标状态,库会自动计算过渡。
layout 动画是 Framer Motion 最强大的特性。当列表项位置变化时,只需要加一个
layoutprop,就会自动有过渡动画,而不是突兀地跳到新位置。关于性能,动画应该尽量只改变
transform和opacity,这两个属性是 GPU 加速的,不会触发重排(reflow)和重绘(repaint)。
延展阅读
- Framer Motion 官方文档 — 完整的 Framer Motion 文档
- Framer Motion: Animation — 动画基础
- Framer Motion: Gestures — 手势动画
- Framer Motion: Layout Animations — Layout 动画
- MDN: CSS animations and transitions performance — CSS 动画性能