React 动画入门

2025-05-27

React 动画入门

交互在塑造用户在应用程序中的体验方面起着关键作用。动画有助于定义这些交互,因为用户的眼睛往往会关注移动的物体。这些引人注目的动感元素讲述了一个故事,帮助应用程序从竞争对手中脱颖而出,并带来更好的用户体验。

创建动画可能令人望而生畏,尤其是编程和处理动画编排(如何协调它们之间的关系)。值得庆幸的是,一些优秀的开发者在库中创建了抽象,使开发人员能够高效地创建无缝的、硬件加速的动画。

在本文中,我将介绍 Framer Motion 并使用它创建简单的动画。我们将学习运动组件、编排、拖动和自动动画。

React 动画库

在 React 中,我们有两个主要的动画库:React SpringFramer motion。我喜欢它们两个,但我相信它们每个都有各自的用途。

React Spring 是一个基于弹簧物理的动画库。这些动画模拟了真实的弹簧物理特性,从而实现流畅的动画效果。它非常强大且灵活。几乎所有 HTML 标签的属性都可以使用 React Spring 实现完整的动画效果。这对于复杂的 SVG 动画尤其重要,然而,它的主要缺点是学习难度较高。

Framer Motion 是一个动效库。它易于学习,并且功能强大,支持多种编排。与 React Spring 不同,它拥有更多类型的动画:弹簧动画、补间动画和惯性动画。补间动画代表类似 CSS 的基于持续时间的动画,而惯性动画则根据初始速度减速,通常用于实现惯性滚动。

Framer Motion 非常适合处理 99% 网站的动画。它的主要缺点是缺乏文档,并且某些属性不适用于 SVG 动画。

在这些库之间进行选择很大程度上取决于你正在构建的内容以及你愿意投入多少时间学习动画。React Spring 可以完成 Framer Motion 的所有功能,并且更加灵活,但它的阅读和理解难度更大。我推荐使用它来处理自定义的复杂动画,尤其是 SVG 和 3D 动画(Three.js)。

对于大多数网站来说,Framer Motion 更胜一筹,因为它能够处理大多数常见情况,而且与 React Spring 相比,学习曲线非常低。此外,它处理动画的方式更加直观、更具声明性。因此,我们将重点介绍这个库,并用它来学习动画。Framer Motion 的基础知识可以移植到 React Spring,但其语法会更加抽象。

工作原理:运动组件

Framer 运动核心 API 是motion组件。每个 HTML 和 SVG 元素都有一个motion组件。它们的工作原理与 HTML 组件完全相同,但具有额外的属性,可以声明式地添加动画和手势。

可以将motion组件视为一个巨大的 JavaScript 对象,可以用来访问所有 HTML 元素。以下是一些调用motion组件的方式:

<motion.div />
<motion.span />
<motion.h1 />
<motion.svg />
...

如前所述,它们允许使用额外的道具。一些最常用的道具是:

  • initial定义元素的初始状态。
  • style像普通的 React 元素一样定义样式属性,但是通过运动值(跟踪组件的状态和速度的值)的任何值的变化都会被动画化。
  • animate定义组件挂载时的动画。如果其值不同于styleinitial,则会自动为这些值设置动画。要禁用挂载动画,initial必须将 设置为false
  • exit定义组件卸载时的动画。仅当该组件是该<AnimatePresence />组件的子组件时才有效。
  • transition允许我们更改动画属性。在这里,我们可以修改动画的持续时间、缓动、动画类型(弹簧动画、补间动画和惯性动画)、持续时间以及许多其他属性。
  • variants允许协调组件之间的动画。

现在我们知道了可以包含的基本道具motion以及如何声明它们,我们可以继续创建一个简单的动画。

坐骑动画

假设我们想要创建一个在 mount 时会淡入淡出的元素。我们会使用initialandanimate属性。

在属性中initial,我们将声明组件在挂载之前的位置。我们将添加一个opacity: 0and y: -50。这意味着组件最初将被隐藏,并且位于其位置上方 50 像素处。

animateprop 中,我们必须声明组件在挂载或显示给用户时的外观。我们希望它可见并位于其初始位置,因此我们添加一个opacity: 1and y: 0

Framer Motion 将自动检测道具initial与具有不同的值animate,并对属性中的任何差异进行动画处理。

我们的代码片段如下:

import { motion } from "framer-motion"

<motion.div
  initial={{ opacity: 0, y: -50 }}
  animate={{ opacity: 1, y: 0 }}  
>
  Hello World!
</motion.div>

这将创建以下动画:

卡片逐渐淡出

恭喜您使用 Framer Motion 创建了您的第一个动画!

💡 你可能已经注意到上面的代码片段缺少样式。为了更好地展现 Framer Motion 的功能,部分代码片段会省略样式。

📕 本文中的所有动画都会有各自的 Storybook。以下是Storybook 的链接。

卸载动画

在创建动态 UI 时,卸载或退出动画至关重要,尤其是在删除项目或处理页面转换时。

要在 Framer Motions 中处理退出动画,第一步是将元素包裹在 中<AnimatePresence/>。这样做的原因是:

  • 没有生命周期方法来通知组件何时被卸载
  • 无法将卸载推迟到动画完成为止。

Animate presence 会自动为我们处理所有这些。

元素被包裹后,必须为其赋予一个exitprop 来指定新的状态。就像animate检测到 中的值差异一样initialexit也会检测到 中的变化animate并相应地进行动画处理。

让我们实践一下!如果我们使用上一个组件并添加一个退出动画。我们希望它退出时具有与初始动画相同的属性。

import { motion } from "framer-motion"

<motion.div
    exit={{ opacity: 0, y: -50 }}
  initial={{ opacity: 0, y: -50 }}
  animate={{ opacity: 1, y: 0 }}    
>
  Hello World!
</motion.div>

现在,让我们添加一个<AnimatePresence/>,以便它可以检测我们的组件何时卸载:

import { motion } from "framer-motion"

<AnimatePresence>
    <motion.div
        exit={{ opacity: 0, y: -50 }}
      initial={{ opacity: 0, y: -50 }}
      animate={{ opacity: 1, y: 0 }}    
    >
      Hello World!
    </motion.div>
</AnimatePresence>

让我们看看当组件卸载时会发生什么:

卡片淡出

📕故事书链接

编排

Framer Motion 的一大优势在于它能够通过变体来协调不同的元素。变体是简单的单组件动画的目标对象。它们可以通过 DOM 传递动画,从而实现元素的协调。

变量通过 prop 传入motion组件variants。它们通常如下所示:

const variants = {
  visible: { opacity: 0, y: -50 },
  hidden: { opacity: 1, y: 0 },
}

<motion.div initial="hidden" animate="visible" variants={variants} />

这些将创建与上面相同的动画。您可能注意到我们传递了initial一个animate字符串。这仅用于变体。它告诉 Framer Motion 应该在变体对象中查找哪些键。对于initial,它将查找“hidden”和animate“visible”。

使用此语法的好处是,当运动组件有子组件时,变体的变化将通过组件层次结构向下流动。它将持续向下流动,直到子组件拥有自己的animate属性。

让我们付诸实践!这次我们将创建一个惊人的列表。像这样:

卡片一张一张地进入。

图中每个物品的进入延迟时间逐渐增加。第一个物品在 0 秒后进入,第二个在 0.1 秒后进入,第三个在 0.2 秒后进入,并以 0.1 秒为单位持续增加。

为了通过变体实现这一点,首先,让我们创建一个变体对象,其中我们将存储所有可能的状态和转换选项:

const variants = {
  container: {  
  },
  card: { 
  }
};

variants.containervariants.card代表motion我们将拥有的每个组件。

让我们为卡片创建动画。我们看到卡片从左到右淡入。这意味着我们必须更新它的x位置和opacity

如上所述,变体可以对其动画状态使用不同的键,但是,我们将其保留为initial和,animate分别表示安装前和安装后。

在 上initial,我们的组件将位于左侧 50 像素处,其不透明度将为 0。

在 上animate,我们的组件将位于左侧 0 像素,其不透明度将为 1。

像这样:

const variants = {
  container: {
  },
  card: {
    initial: {
      opacity: 0,
      x: -50
    },

    animate: {
      opacity: 1,
      x: 0
    }
  }
};

接下来,我们需要为每张卡片添加交错效果。为此,我们需要添加一个container.transition属性,用于更新动画的行为。在该属性中,我们将添加一个staggerChildren属性,用于定义子卡片动画之间的增量延迟。

const variants = {
  container: {
        animate: {
      transition: {
        staggerChildren: 0.1
      }
    }
  },
  card: {
    initial: {
      opacity: 0,
      x: -50
    },

    animate: {
      opacity: 1,
      x: 0
    }
  }
};

现在,如果我们将此变体挂接到motion组件上:

import { motion } from "framer-motion";

const variants = {
  container: {
    animate: {
      transition: {
        staggerChildren: 0.1
      }
    }
  },
  card: {
    initial: {
      opacity: 0,
      x: -50
    },

    animate: {
      opacity: 1,
      x: 0
    }
  }
};

const StaggeredList = () => {
  return (
    <motion.div
      initial="initial"
      animate="animate"
      variants={variants.container}     
    >
      {new Array(5).fill("").map(() => {
        return <Card />;
      })}
    </motion.div>
  );
};

const Card = () => (
  <motion.div
    variants={variants.card}   
  >
    Hello World!
  </motion.div>
);

至此,我们的动画已完成,流畅的交错列表已准备就绪!

卡片一张一张地进入。

📕故事书链接

拖拽

拖拽功能在应用中实现起来可能比较困难。幸好,Framer Motion 的声明式特性让实现拖拽逻辑变得简单很多。本文我将简单介绍一下它。不过,在之后的教程中,我可能会更详细地讲解如何创建更复杂的拖拽操作,比如删除幻灯片。

让元素可拖动非常简单:drag给组件添加一个 prop motion。例如:

import { motion } from "framer-motion";

<motion.div drag>
  Hello World!
</motion.div>

添加drag属性将使其在 x 轴和 y 轴上可拖动。需要注意的是,您可以通过将所需的轴提供给 来将移动限制在单个轴上drag

仅设置属性存在问题drag。它没有绑定到任何区域或容器,因此它可以像这样移动到屏幕之外:

卡片被拖到屏幕外

为了设置约束,我们给dragContraints一个对象赋予每个方向所需的约束:topleftrightbottom。例如:

import { motion } from "framer-motion";

<motion.div
  drag
  dragConstraints={{
    top: -50,
    left: -50,
    right: 50,
    bottom: 50
  }}
>
  Hello World!
</motion.div>

这些约束允许元素在任何方向上最多移动 50 像素。如果我们尝试将其拖动到顶部,例如 51 像素,它将被停止并弹回。像这样:

卡片被拖动并与约束发生碰撞

就好像有一堵方形的无形墙,阻止组件进一步移动。

📕故事书链接

布局属性

Proplayout是 Framer Motion 中一个强大的功能。它允许组件在布局之间自动添加动画。它会检测元素样式的变化并进行动画处理。它有多种用途:重新排序列表、创建开关等等。

赶紧用起来!我们来做一个 switch。首先,我们来创建初始标记

import { motion } from "framer-motion";

const Switch = () => {
  return (
    <div
      className={`flex w-24 p-1 bg-gray-400 bg-opacity-50 rounded-full cursor-pointer`}
      onClick={toggleSwitch}
    >
            {/* Switch knob */}
      <motion.div
        className="w-6 h-6 p-6 bg-white rounded-full shadow-md"
        layout       
      ></motion.div>
    </div>
  );
};

现在,让我们添加逻辑:

import { motion } from "framer-motion";

const Switch = () => {
    const [isOn, setIsOn] = React.useState(false);

  const toggleSwitch = () => setIsOn(!isOn);

  return (
    <div onClick={toggleSwitch}>
            {/* Switch knob */}
      <motion.div       
        layout       
      ></motion.div>
    </div>
  );
};

你可能注意到了,只有我们的旋钮有这个layout属性。只有我们希望动画的元素才需要这个属性。

我们希望旋钮从一侧移动到另一侧。我们可以通过改变容器的弹性对齐方式来实现。当开关打开时,布局将具有justify-content: flex-end。Framer Motion 会感知旋钮位置的变化,并相应地为其位置添加动画效果。

让我们将其添加到我们的代码中:

import { motion } from "framer-motion";

const Switch = () => {
  const [isOn, setIsOn] = React.useState(false);

  const toggleSwitch = () => setIsOn(!isOn);

 return (
    <div
      style={{
         background: isOn ? "#48bb78" : "rgba(203, 213, 224, 0.5)",
        justifyContent: isOn && "flex-end",
        width: "6rem",
        padding: "0.25rem",
        display: "flex",
        borderRadius: 9999,
        cursor: "pointer",   
      }}
      onClick={toggleSwitch}
    >
            {/* Switch knob */}
      <motion.div
        style={{
          width: "3rem",
          height: "3rem",
          background: "white",
          borderRadius: "100%",
          boxShadow:
            "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
        }}
        layout       
      ></motion.div>
    </div>
  );
};

我添加了一些其他样式,让它看起来像一个开关。总之,结果如下:

正在切换的开关

太棒了!Framer Motion 竟然能自动完成这些操作,无需处理额外的控件,真是太神奇了。不过,与我们在“设置”等应用中看到的相比,它看起来有点乏味。我们可以通过添加一个transitionprop 来快速解决这个问题。

import { motion } from "framer-motion";

const Switch = () => {
 const [isOn, setIsOn] = React.useState(false);

 const toggleSwitch = () => setIsOn(!isOn);

 return (
    <div
      style={{
         background: isOn ? "#48bb78" : "rgba(203, 213, 224, 0.5)",
        justifyContent: isOn && "flex-end",
        width: "6rem",
        padding: "0.25rem",
        display: "flex",
        borderRadius: 9999,
        cursor: "pointer",   
      }}
      onClick={toggleSwitch}
    >
            {/* Switch knob */}
      <motion.div
        style={{
          width: "3rem",
          height: "3rem",
          background: "white",
          borderRadius: "100%",
          boxShadow:
            "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
        }}
        layout    
                transition={{
          type: "spring",
          stiffness: 500,
          damping: 30,
        }}   
      ></motion.div>
    </div>
  );
};

我们定义了弹簧类型的动画,因为我们想要一种有弹性的感觉。

定义stiffness了旋钮的移动看起来有多么突然。

并且,damping定义了类似于摩擦力的反作用力的强度。这意味着它停止移动的速度有多快。

这些结合在一起产生了以下效果:

利用弹簧效应来切换开关。

现在我们的开关看起来更加生动了!

📕故事书链接

结论

创建动画可能令人望而生畏,尤其是在许多库都包含复杂的术语的情况下。值得庆幸的是,Framer Motion 允许开发者通过其声明式且直观的 API 创建无缝动画。

这篇文章旨在介绍 Framer Motion 的基础知识。在后续的文章中,我将创建一些复杂的动画,例如滑动展开和删除、抽屉式布局、共享布局等等。如果您对动画有什么建议,请在评论区告诉我!

想要了解更多最新的 Web 开发内容,请在TwitterDev.to上关注我!感谢阅读!😎


你知道我有一份新闻通讯吗?📬

如果您想在我发布新博客文章时收到通知并收到精彩的每周资源以保持网络开发领先地位,请访问https://jfelix.info/newsletter

文章来源:https://dev.to/joserfelix/getting-started-with-react-animations-308a
PREV
完整的 Flexbox 教程(带备忘单)目录 - FlexBox 架构 FlexBox 图表 - 如何设置项目 flex-direction justify-content align-content align-items align-self flex - grow | shrink | wrap | basis 简写总结
NEXT
如何管理多个 SSH 密钥对