📖 核心概念
- Framer Motion核心概念
- animate与transition属性
- Variants与编排动画
- 布局动画与共享布局
- 手势交互与拖拽
- 页面过渡与路由动画
💻 代码实现
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>
);
}
🎯 练习任务
- 1 实现列表项的交错进入动画
- 2 创建一个可拖拽的卡片堆叠(类似Tinder)
- 3 实现共享布局动画,点击列表项展开详情
- 4 添加页面路由过渡动画
🏆 成就解锁
动画艺术家 — 掌握Framer Motion,创造流畅优雅的React动画