用途
本技能用于在 Framer Motion 中实现布局动画,主要覆盖三类场景:共享元素过渡、布局重排(reordering)动画,以及协同的挂载/卸载动画。当用户询问 Framer Motion 布局动画、layoutId 或 AnimatePresence 时适用。
相关技能:核心动画使用 framer-motion-core;变体使用 framer-motion-variants;React 集成使用 framer-motion-react。
主要能力
layout 属性
layout 属性在布局变化时启用自动位置动画:
<motion.div layout>
{items.map(item => (
<motion.div key={item.id} layout />
))}
</motion.div>
布局模式
| 模式 | 行为 |
|---|---|
| true | 同时动画位置与尺寸 |
| "position" | 仅动画位置 |
| "size" | 仅动画尺寸 |
<motion.div layout="position" />
<motion.div layout="size" />
用 layoutId 实现共享元素过渡
layoutId 可在不同组件中的元素之间实现平滑过渡。
页面过渡:
// Page A
function CardA() {
return <motion.div layoutId="card" />;
}
// Page B
function CardB() {
return <motion.div layoutId="card" />;
}
当 CardA 卸载、CardB 以相同 layoutId 挂载时,Framer Motion 会在两个位置之间平滑动画该元素。
模态浮层:
function ListItem({ item, onClick }) {
return (
<motion.div layoutId={`item-${item.id}`} onClick={onClick}>
{item.name}
</motion.div>
);
}
function Modal({ item }) {
return (
<motion.div layoutId={`item-${item.id}`}>
<h2>{item.name}</h2>
<p>{item.description}</p>
</motion.div>
);
}
用于布局的 AnimatePresence
AnimatePresence 可启用退出动画:
import { AnimatePresence, motion } from "framer-motion";
function TodoList({ todos }) {
return (
<motion.ul>
<AnimatePresence>
{todos.map(todo => (
<motion.li
key={todo.id}
layout
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
/>
))}
</AnimatePresence>
</motion.ul>
);
}
AnimatePresence 模式
<AnimatePresence mode="wait">
{isOpen && <Modal key="modal" />}
</AnimatePresence>
| 模式 | 说明 |
|---|---|
| "sync" | 所有动画同时运行(默认) |
| "wait" | 退出完成后才开始进入 |
| "popLayout" | 退出的元素立刻从布局中移除 |
可重排列表
结合 layout 与 drag 可实现可重排列表:
function ReorderableList({ items, setItems }) {
return (
<AnimatePresence>
{items.map(item => (
<motion.div
key={item.id}
layout
drag
dragConstraints={{ top: 0, bottom: 0 }}
onDragEnd={({ info, target }) => {
// Calculate new index and update
}}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
/>
))}
</AnimatePresence>
);
}
网格中的共享布局
function Grid({ items }) {
return (
<motion.div
layout
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))",
gap: 10
}}
>
{items.map(item => (
<motion.div
key={item.id}
layout
style={{
width: "100%",
aspectRatio: 1,
backgroundColor: item.color
}}
/>
))}
</motion.div>
);
}
退出动画
使用 AnimatePresence 的元素必须定义退出状态:
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
/>
结合 layout 的退出:
<motion.div
layout
exit={{ opacity: 0, x: -100, transition: { duration: 0.3 } }}
/>
用 layoutId 实现交叉淡化(crossfade)
当多个元素在同一层级共享 layoutId 时:
function Toggle() {
const [showA, setShowA] = useState(true);
return (
<AnimatePresence mode="popLayout">
{showA ? (
<motion.div
key="a"
layoutId="shape"
style={{ backgroundColor: "red" }}
/>
) : (
<motion.div
key="b"
layoutId="shape"
style={{ backgroundColor: "blue", borderRadius: "50%" }}
/>
)}
</AnimatePresence>
);
}
该元素会在各状态之间平滑变形,包括 border radius。
使用方式(最佳实践)
- ✅ 使用 layoutId 实现路由/页面之间的共享元素过渡。
- ✅ 为动画列表包裹 AnimatePresence 以启用退出动画。
- ✅ 使用 layout 属性实现自动位置动画。
- ✅ 使用 AnimatePresence mode="popLayout" 实现平滑重排。
- ✅ 为使用 AnimatePresence 的组件定义退出状态。
- ✅ 当子元素需要动画位置时,在父容器上使用 layout。
避免事项:
- ❌ 忘记为条件渲染的动画组件包裹 AnimatePresence。
- ❌ 在同一层级的多个元素上使用相同的 layoutId。
- ❌ 忘记为会卸载的组件定义退出状态。
- ❌ 在子元素缺少正确 key 的情况下使用布局动画。
- ❌ 同时动画过多布局元素——应分组或错开(stagger)。
适用场景
- 共享元素过渡(如卡片到详情的展开)。
- 页面/路由切换过渡。
- 模态浮层与列表项的联动。
- 列表重排(配合 drag)。
- 网格布局变化动画。
- 条件渲染内容的进入/退出动画。
限制
资料未提供计费、价格、平台限制或效果指标等信息,这些事实暂未提供。
了解更多
- https://www.framer.com/motion/layout-animations/
- https://www.framer.com/motion/animate-presence/