如果动画执行得当,它们可以提供强大的用户体验。然而,尝试使用 CSS 创建出令人惊叹的动画可能会令人伤脑筋。许多动画库可以简化这一过程,但大多数动画库不够全面,无法构建复杂的动画。
在本文中,我们将演示如何使用 Framer Motion 创建滚动动画,这是一个动画库,无需成为 CSS 专家也可创作出精美的动画。
前置条件
若要充分理解本教程,应具备以下条件:
- 熟悉 React 及其概念(包括 Hooks)
- CSS 属性(如不透明度、过渡和缩放)相关知识
无需具备任何 Framer Motion 的先验知识。本文将介绍该库的基本概念。
让我们先了解一下 Framer Motion 和 Intersection Observer 功能的背景知识。
什么是 Framer Motion?
Framer Motion 是一个动画库,在 React 中可以声明性创建动画。它提供可供生产的动画和 API,以帮助简化将动画集成到应用程序中的过程。
一些 React 动画库,如 react-transition-group 和 transition-hook,使用手动配置的 CSS 对元素进行动画处理。Framer Motion 采用了一种不同的方法,即使用预配置的样式在引擎盖下对元素进行动画处理。
motion
和 useAnimation
是 Framer Motion 用于触发和控制这些样式的两个功能。该 motion
函数用于创建运动组件,这些组件是 Framer Motion 的构建块。
通过 motion
为 HTML 或 SVG 元素添加前缀,该元素会自动成为组件:
html
<motion.h1>Motion Component</motion.h1>
组件可以访问多个属性,包括 animate
属性。 animate
接收具有要进行动画处理的组件的已定义属性的对象。当组件装载时,对象中定义的属性将进行动画处理。
在 React 中使用 Framer Motion 创建滚动动画
使用 Framer Motion 在 React 应用程序中创建滚动动画有两种主要方法:scroll-triggere
或 scroll-linked
动画。
scroll-triggered
动画包括检测元素在视口中何时可见并激活与之关联的动画。我们可以使用 Intersection Observer API 或 Framer Motion 的 whileInView
钩子来实现这一点。
另一方面,scroll-linked
动画涉及跟踪页面的滚动位置并使用它来控制元素的动画进度。
在本教程中,我们将探讨如何使用这两种方法。让我们开始吧。
创建滚动触发的动画
在引入钩子之前 whileInView
,Framer Motion 没有在元素进入或离开视口时观察元素的功能。因此,元素在装载到 DOM 后会立即动画化。
为了解决这个问题,开发人员将使用原生 Javascript API,例如 Intersection Observer API,它可以防止元素在定义的边界内或视口内进行动画处理。
使用 Intersection Observer API
Intersection Observer API 是一个 JavaScript API,它提供了一种异步观察目标元素与顶级文档视口相交的变化的方法。它注册一个回调函数,每当我们想要监视的元素进入或退出另一个元素或视口时,就会执行该函数。
在本文中,我们将使用 react-intersection-observer 库,这是 Intersection Observer API 的 React 实现。此库旨在使用较少的样板代码处理上述功能。它提供了 Hooks 和渲染,可以轻松跟踪视口上元素的滚动位置。
react-intersection-observer 是一个相对较小的包,因此无需担心它可能会给您的项目增加开销:
现在,让我们设置一个简单的 React 项目并安装必要的依赖项。
设置我们的 React 项目
我们将通过安装 React 来开始我们的项目设置:
bash
npm create vite@latest my-app -- --template react
然后,我们将安装 Framer Motion 和 react-intersection-observer
:
bash
npm i react-intersection-observer framer-motion
接下来,我们将使用 Framer Motion 和 react-intersection-observer 库设置一个演示 React 应用程序。
创建演示 React 应用程序
首先,我们将创建一个 box 组件,它可以是卡片、模态或其他任何东西。我们将这个 box 组件导入到主组件 . App.js
当这个组件进入视口时,我们将对其进行动画处理:
js
/*Box component*/
const Box = () => {
return (
<div className="box">
<h1>Box</h1>
</div>
);
};
/*Main component*/
export default function App() {
return (
<div className="App">
<Box /> /*imported Box component*/ /*imported Box component*/
</div>
);
}
接下来,我们将从之前安装的库中导入创建动画所需的所有其他内容:
js
motion and useAnimation Hooks from Framer Motion
useEffect Hook from React
useInView Hook from react-intersection-observer
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
这些是我们需要对 box 组件进行动画处理的基本 Hooks。在本教程的后面部分,将了解每个 Hook 的工作原理。
在我们的组件内部是一个 div
元素 box
,其 className
为 。为了对 box
元素进行动画处理,我们需要使其成为运动组件。我们通过在元素前面添加 motion
前缀来做到这一点:
js
const Box = () => {
return (
<motion.div className="box">
<h1>Box</h1>
</motion.div>
);
};
我们可以通过向 motion
组件添加 initial
和 animate
属性。
js
<motion.div
animate={{ x: 100 }}
initial={{x: 0}}
className="box"
></motion.div>
对于更复杂的动画,Framer Motion 提供了变体。接下来让我们看看如何使用此功能。
使用变体制作动画
变体是一组预定义的对象,让我们以声明方式定义动画的外观。它们具有我们可以在动画组件中引用的标签来创建动画。
下面是 variant 对象的示例:
js
const exampleVariant = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}
在这个变体对象中, exampleVariant
有两个属性: visible
和 hidden
。这两个属性都传递一个对象作为值。当元素 是 visible
时,我们希望 opacity
1
是 ;当它是 hidden
时,我们希望它是 0
。
上面的变体对象可以在动画组件中引用,如下所示:
js
<motion.div variants={exampleVariant} />
接下来,我们将创建一个变体,并将其作为道具传递给我们的动画组件:
js
const boxVariant = {
visible: { opacity: 1, scale: 2 },
hidden: { opacity: 0, scale: 0 },
}
为了在我们的动画组件中引用这个变体对象,我们将向动画组件添加一个 variants
配置:
js
<motion.div
variants={boxVariant}
className="box"
/>
现在,我们的动画组件没有发生任何事情------它可以访问对象,但它不知道如何处理它。动画组件需要一种方法来知道何时开始和结束变体对象中定义的动画。
为此,我们将 initial
and animate
配置传递给 motion 组件:
js
<motion.div
variants={boxVariant}
className="box"
initial="..."
animate="..."
/>
在上面的代码中, initial
配置定义了 motion
组件在挂载之前的行为,而 animate
配置用于定义挂载时的行为。
现在,我们将通过将组件的 设置为装载 0
前和安装 1
时,为组件 opacity
添加淡入动画效果。该 transition
属性具有 duration
一个值,该值指示动画的持续时间:
js
<motion.div
className="box"
initial={{ opacity: 0, transition:{duration: 1}}}
animate={{opacity: 1}}
/>
我们可以通过引用我们之前创建的 variant 对象中的 hidden
和 visible
属性来动态设置它们:
js
const boxVariant = {
visible: { opacity: 1, scale: 2 },
hidden: { opacity: 0, scale: 0 },
}
...
<motion.div
variants={boxVariant}
initial="hidden"
animate="visible"
className="box"
/>
运动组件将继承变体对象 hidden
和 visible
属性的值,并相应地进行动画处理:
现在我们已经有了组件的工作动画,下一步是使用 react-intersection-observer 库访问 Intersection Observer API 并在组件处于视图中时触发动画。
使用 useInView
和 useAnimation
Hooks 添加滚动显示动画
如前所述,Framer Motion 在元素加载时对元素进行动画处理,因此在元素处于视图中时,我们需要能够控制它们何时安装和卸载。
useAnimation
Hook 提供了帮助程序方法,让我们可以控制动画发生的顺序。例如,我们可以使用control.start
和 control.stop
方法来手动启动和停止动画。
useInView
是一个 react-intersection-observer Hook,它允许我们跟踪组件在视口中何时可见。这个 Hook 让我们可以访问 一个 ref
,我们可以将其传递给我们想要观看的组件,它告诉我们组件是否在视口中。
例如,如果我们将组件作为参数传递给 ref
。当组件滚动到视口和离开视口,在控制台,将显示true
或false
:
现在,我们将使用 useAnimation
Hook 在运动组件进入视口时触发动画。
首先,我们将从 useInView
Hook 中引用 ref
and inView
,并分配给 useAnimation
一个变量:
js
const control = useAnimation()
const [ref, inView] = useInView()
接下来,我们将 ref
添加到我们的 motion 组件中,并将变量 control
作为值传递给 animate
:
js
<motion.div
ref={ref}
variants={boxVariant}
initial="hidden"
animate={control}
className="box"
/>
最后,我们将创建一个 useEffect
Hook,以便在我们看到的组件出现在视野中时调用该 control.start
方法。在这个 Hook 中,我们将 control
和 inView
变量作为依赖项传递:
js
useEffect(() => {
if (inView) {
control.start("visible");
}
}, [control, inView]);
在 useEffect
回调函数中,我们使用 if
语句执行条件检查,以检查运动组件是否在视图中。
如果条件是 true
, useEffect
将调用 control.start
带有 "visible"
值的方法。这将触发动画组件上的 animate
属性并启动动画。
现在,如果我们上下滚动视口,当框组件的滚动位置进入视口时,它们将进行动画处理:
请注意,框组件仅在第一次进入视口时才进行动画处理。
我们可以通过在 useEffect
回调函数中向 if
语句添加一个 else
块来使它们每次出现在视图中时出现动画。在这个 else
块中,我们将再次调用该 control.start
方法,但这次将一个 "hidden"
值传递给它:
js
else {
control.start("hidden");
}
现在,如果我们上下滚动视口,则每次滚动位置进入视口时,盒子组件都会进行动画处理:
以下是在 React with Framer Motion 中创建滚动触发动画的最终代码:
js
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
const boxVariant = {
visible: { opacity: 1, scale: 1, transition: { duration: 0.5 } },
hidden: { opacity: 0, scale: 0 }
};
const Box = ({ num }) => {
const control = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
control.start("visible");
} else {
control.start("hidden");
}
}, [control, inView]);
return (
<motion.div
className="box"
ref={ref}
variants={boxVariant}
initial="hidden"
animate={control}
>
<h1>Box {num} </h1>
</motion.div>
);
};
export default function App() {
return (
<div className="App">
<Box num={1} />
<Box num={2} />
<Box num={3} />
</div>
);
}
使用 Framer Motion whileInView
prop
Intersection Observer API 专为检测复杂的交叉行为而定制,可让你对交叉变化进行精细控制。但是,假设你需要一种简化的、特定于 React 的方法来处理滚动触发的动画。
在这种情况下,Framer Motion 提供了一个为 motion 组件调用 whileInView
的属性,我们可以使用它来触发滚动触发的动画。该 whileInView
定义了一组属性或变体标签,这些属性或变体标签在元素处于视图中时对其进行动画处理:
js
<motion.div whileInView={{ scale: 1 }} initial={{ scale: 0 }}>...<motion.div/>
在此示例中,组件的比例设置为在装载之前设置为0
,加载完成以后设置成1
。
js
const variant = {
visible: { scale: 1 },
hidden: { scale: 0 },
};
<motion.div
variant="variant"
initial="hidden"
whileInView="visible"
>...<motion.div/>
如果我们在用例中实现,例如在博客网站上对图像进行动画处理,结果将如下所示:
可以在 Code Sandbox 上找到此演示并与之交互。
正如你所看到的,该 whileInView
道具是一种更紧凑的方法,用于通过 Framer Motion 实现滚动触发功能。相比之下,交叉点观察器需要额外的步骤来设置。
whileInView
的关联属性提供了一定更简便的方法:
viewport
:定义如何检测视口的对象。它接受一组属性,我们可以使用这些属性来定义whileInView
道具被触发的次数,选择要跟踪的视口,并在检测元素的可见性时向视口添加边距onViewportEnter
:元素进入视口时触发的回调。它在后台使用 Intersection Observer API 的intersectionObserverEntry
接口来处理此功能onViewportLeave
:与回调相反。onViewportEnter
当元素离开视口时触发
可以在官方 Framer Motion 文档中了解有关这些属性的更多信息。
带有 useScroll
钩子的滚动链接动画
滚动链接动画是 Web 上最常见的滚动动画类型。这些动画由用户在网页上的滚动行为触发。将动画与特定的滚动位置同步,可在用户滚动浏览 Web 应用的内容时创建引人入胜的用户体验。
滚动链接动画的一些常见用例包括视差效果、滚动进度指示器等。
Framer Motion 提供了一个 useScroll
钩子,可以轻松创建滚动链接的动画。 useScroll
钩子返回四个运动值,我们可以根据元素的滚动状态使用动画元素:
scrollX
和scrollY
,表示页面或元素在x
和y
轴上的绝对滚动位置scrollXProgress
和scrollYProgress
,表示相对于定义的偏移量的滚动进度
偏移量是介于 和 1
之间的 0
数组,其中 0
表示元素在视口之外, 1
表示元素在视口内。
我们可以使用偏移量来计算目标元素和视口相交的点。也可以使用一组字符串来定义它们: start
、 和 end
,它们分别表示 0
、 0.5
center
和 1
。
让我们看一个示例,说明如何使用运动值来创建滚动指示器。我们只需要将 scrollYProgress
值传递给进度条元素的 styleX
style 属性,如下面的代码示例所示:
js
const { scrollYProgress } = useScroll();
<motion.div className="progress-bar" style={{ scaleX: scrollYProgress }} />
效果如下:
可以使用 CodeSandbox 与演示进行交互。
useScroll
值可以与其他钩子(如 useTransform
和 useSpring
)结合使用,以组成复杂的动画,如下例所示:
js
function Images({ text, url }) {
const ref = useRef(null);
const { scrollYProgress } = useScroll({ target: ref });
const y = useTransform(scrollYProgress, [0, 1], [-300, 350]);
return (
<section>
<div ref={ref}>
<img src={url} alt={text} />
</div>
<motion.h2 style={{ y }}>{text}</motion.h2>
</section>
);
}
在此示例中,我们使用 useTransform
hook 根据页面每个部分中图像的滚动位置创建文本的视差效果:
要检查这是如何完成的,可以在CodeSandbox上打开演示并检查代码。
将 Framer Motion 与三种替代方案进行比较
为 Web 应用程序创建动画时,决定使用哪个库可能具有挑战性。尽管有许多动画库可供选择,但这些库中只有少数提供内置功能来对滚动元素进行动画处理。
那么问题就变成了为什么你应该使用Framer Motion而不是这些替代方案。有许多因素需要考虑,但一般来说,Framer Motion 在简单性和复杂性之间取得了平衡,对于希望实现简单动画并实现缩放的开发人员来说,这是正确的选择。
为了更清楚地了解这种平衡,让我们将 Framer Motion 与三个提供滚动动画功能的库进行比较:GSAP、React Animate on Scroll 和 React Reveal。
Framer Motion 与 GSAP 对比
GSAP 是一个功能强大的动画库,它为创建动画提供了强大的工具集。它的 ScrollTrigger API 专注于基于滚动的动画。您可以使用它来创建高级滚动触发动画,并精确控制滚动触发器和时间线。
与 Framer Motion 相比,GSAP 的 ScrollTrigger 更加灵活和强大。但是,它的学习曲线更陡峭,设置起来需要更多努力,而 Framer Motion 则更简单。
Framer Motion 与 React Animate on Scroll
React Animate On Scroll是一个轻量级动画库,它提供了一个简单的界面,用于在元素进入视口时触发动画。
与其他库(如 Framer Motion 和 GSAP)相比,该库在动画自定义方面不太灵活。但是,它确实简化了基于滚动位置对元素进行动画处理的过程。
Framer Motion 与 React Reveal
与 React Animate On Scroll 类似,React Reveal 库也提供了一个简单的界面,用于根据滚动位置对元素进行动画处理。然而,React Reveal 不允许用户定义他们的动画,而是提供了一组预定义的动画,很容易地应用于元素中。
这种简单性使 React Reveal 成为开发人员的不错选择,他们希望以一种快速简便的方式将动画添加到他们的 React 应用程序中。然而,这也意味着 React Reveal 的灵活性不如 Framer Motion 和 GSAP 等综合动画库。
原文: Implementing React scroll animations with Framer Motion - LogRocket Blog