Skip to content

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的作用是:

  1. 共享动画:当一个组件在React的渲染过程中被移除并替换为另一个组件时,如果这两个组件具有相同的layoutId,Framer Motion会在它们之间共享动画。这意味着即使组件被替换,动画也会看起来是连续的。
  2. 流畅的过渡:在组件的进入和退出之间提供流畅的过渡效果。通过使用相同的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 App

AnimatePresence与退出动画 ​

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的进阶特性:

  • 变体系统的高级用法
  • 复杂手势交互
  • 布局动画
  • 退出动画
  • 实际案例实现

相关资源 ​