Framer Motion进阶指南
动画变体(Variants)深入解析
变体系统是Framer Motion最强大的特性之一,它让我们能够更好地组织和控制复杂的动画序列。
1. 变体的传播
父组件的变体状态会自动传播给子组件:
jsx
const containerVariants = {
hidden: {
opacity: 0,
y: 20
},
visible: {
opacity: 1,
y: 0,
transition: {
// 控制子元素动画
staggerChildren: 0.1, // 子元素依次执行的间隔
delayChildren: 0.3, // 子元素开始前的延迟
}
}
}
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 }
}
function AnimatedList() {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{[1, 2, 3].map(item => (
<motion.div
key={item}
variants={itemVariants}
// 同名变量不需要指定initial和animate
// 会自动继承父组件的写法,然后用子变体的对应值
>
Item {item}
</motion.div>
))}
</motion.div>
)
}2. 动态变体
变体可以是函数,这让我们能够基于props动态生成动画状态:
jsx
const dynamicVariants = {
hidden: { opacity: 0, x: -20 },
visible: (custom) => ({
opacity: 1,
x: 0,
transition: {
delay: custom * 0.2
}
})
}
function DynamicList() {
return (
<div>
{[1, 2, 3].map(index => (
<motion.div
key={index}
custom={index}
variants={dynamicVariants}
initial="hidden"
animate="visible"
>
Item {index}
</motion.div>
))}
</div>
)
}手势动画进阶
1. 拖拽功能
jsx
function DraggableCard() {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<motion.div
drag
dragConstraints={{ // 限制拖拽范围
top: -50,
left: -50,
right: 50,
bottom: 50,
}}
dragElastic={0.2}
dragMomentum={false}
onDragEnd={(event, info) => {
setPosition({ x: info.point.x, y: info.point.y });
console.log(info.point.x, info.point.y);
}}
style={{
width: 100,
height: 100,
background: '#1a73e8',
borderRadius: 10
}}
/>
)
}2. 高级手势检测
jsx
function AdvancedGestures() {
return (
<motion.div
drag
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
whileDrag={{ rotate: 15 }}
onHoverStart={e => console.log("Hover starts")}
onHoverEnd={e => console.log("Hover ends")}
onTapStart={e => console.log("Tap starts")}
onTap={e => console.log("Tap")}
onTapCancel={e => console.log("Tap cancelled")}
style={{
width: 100,
height: 100,
background: '#34a853',
borderRadius: 10
}}
/>
)
}布局动画
1. layout属性
layout属性能够自动动画化布局变化:这个小道具可以使之前可扩展的CSS值动画化,比如在flex-start和flex-end之间切换justify-content。
jsx
function App() {
const [isExpanded, setIsExpanded] = useState(false);
return (
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{ display: 'flex', gap: 20, width: '100%', justifyContent: isExpanded ? "flex-start" : "flex-end", alignItems: 'center' }}>
<motion.div
layout
style={{
width: 150,
height: 150,
background: '#ea4335',
borderRadius: 10
}}
transition={{ duration: 0.3 }}
/>
<motion.div
layout
style={{
background: '#fbbc05',
width: 150,
height: 150,
borderRadius: 10,
transition: { duration: 0.3 }
}}
/>
</div>
)
}2. layoutId共享动画
使用layoutId创建元素之间的共享动画,具体来说,layoutId的作用是:
- 共享动画:当一个组件在React的渲染过程中被移除并替换为另一个组件时,如果这两个组件具有相同的layoutId,Framer Motion会在它们之间共享动画。这意味着即使组件被替换,动画也会看起来是连续的。
- 流畅的过渡:在组件的进入和退出之间提供流畅的过渡效果。通过使用相同的layoutId,Framer Motion可以计算出组件在进入和退出时的布局变化,并自动生成过渡动画。
jsx
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
function App() {
const [todos, setTodos] = useState([
{ id: '1', text: '学习React' },
{ id: '2', text: '完成项目' },
{ id: '3', text: '阅读一本书' }
]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, { id: Date.now().toString(), text: inputValue }]);
setInputValue('');
}
};
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div style={{ padding: 20, maxWidth: 400, margin: '0 auto' }}>
<h2 style={{ textAlign: 'center' }}>待办事项</h2>
<div style={{ display: 'flex', marginBottom: 20 }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
style={{ flex: 1, padding: 10, borderRadius: 5, border: '1px solid #ccc' }}
/>
<button onClick={addTodo} style={{ marginLeft: 10, padding: '10px 20px', borderRadius: 5, background: '#34a853', color: '#fff', border: 'none' }}>
添加
</button>
</div>
<AnimatePresence>
{todos.map(todo => (
<motion.div
key={todo.id}
layoutId={todo.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: 10,
marginBottom: 10,
background: '#f1f3f4',
borderRadius: 5,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}
>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)} style={{ background: 'transparent', border: 'none', color: '#ea4335', cursor: 'pointer' }}>
删除
</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
}
export default AppAnimatePresence与退出动画
AnimatePresence允许我们为组件的移除添加动画效果:
jsx
function ExitAnimation() {
const [isVisible, setIsVisible] = useState(true);
return (
<>
<button onClick={() => setIsVisible(!isVisible)}>
Toggle
</button>
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration: 0.3 }}
style={{
width: 100,
height: 100,
background: '#4285f4',
borderRadius: 10
}}
/>
)}
</AnimatePresence>
</>
)
}实战案例:图片画廊
结合上述特性,创建一个交互式图片画廊:
jsx
function ImageGallery() {
const [selectedId, setSelectedId] = useState(null);
const images = [
{ id: '1', url: 'image1.jpg' },
{ id: '2', url: 'image2.jpg' },
{ id: '3', url: 'image3.jpg' }
];
return (
<div style={{ display: 'grid', gap: 20, gridTemplateColumns: 'repeat(3, 1fr)' }}>
{images.map(image => (
<motion.div
key={image.id}
layoutId={image.id}
onClick={() => setSelectedId(image.id)}
whileHover={{ scale: 1.05 }}
style={{
cursor: 'pointer',
borderRadius: 10,
overflow: 'hidden'
}}
>
<img src={image.url} alt="" style={{ width: '100%', height: 'auto' }} />
</motion.div>
))}
<AnimatePresence>
{selectedId && (
<motion.div
layoutId={selectedId}
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
onClick={() => setSelectedId(null)}
>
<motion.img
src={images.find(img => img.id === selectedId)?.url}
style={{
maxWidth: '90%',
maxHeight: '90vh',
borderRadius: 10
}}
/>
</motion.div>
)}
</AnimatePresence>
</div>
);
}小结
本文深入探讨了Framer Motion的进阶特性:
- 变体系统的高级用法
- 复杂手势交互
- 布局动画
- 退出动画
- 实际案例实现