介绍 Framer Motion

已发表: 2022-03-10
快速总结↬动画,如果做得好,是强大的。 但是,使用 CSS 创建引人注目的动画可能会很棘手。 进来了 Framer Motion。 使用 Framer Motion,您无需成为 CSS 专家即可制作精美的动画。 Framer Motion 为我们提供了生产就绪的动画和一个低级 API,我们可以与之交互以将这些动画集成到我们的应用程序中。

在本文中,我们将仔细研究 Framer Motion 如何帮助我们创建出色的动画。 我们将学习运动组件是如何工作的,并学习如何将动画链接在一起。 我们将研究如何使用 Framer 动作制作手势触发、定时和滚动动画。 在此过程中,我们将使用我们学到的东西来构建我设置的五个演示应用程序,以向我们展示如何将 Framer Motion 集成到实际应用程序中。

本教程将对有兴趣在其 React 应用程序中集成动画感兴趣的读者有所帮助。

注意:本文需要对 React 和 CSS 有基本的了解。

什么是成帧器运动?

Framer Motion 是一个动画库,可以轻松创建动画。 其简化的 API 帮助我们抽象出动画背后的复杂性,并允许我们轻松地创建动画。

运动组件

这些是 Framer 运动的构建块。 运动组件是通过在常规 HTML 和 SVG 元素(例如motion.h1 )前添加motion前缀来创建的。 运动组件可以接受多个道具,基本的一个是animate道具。 这个道具接受一个对象,我们在其中定义我们想要动画的组件的属性。 当组件安装在 DOM 中时,我们定义的属性将被动画化。

让我们使用 Framer Motion 为 h1 文本设置动画。 首先,我们安装 framer-motion 库并导入motion

 npm i framer-motion import { motion } from 'framer-motion';

然后我们将 h1 转换为运动分量。

 <motion.h1 animate={{x: 20, y: -20}}> This is a motion component </motion.h1>

这将导致h1在加载时向右滑动 20px 并向上移动 20px。 如果不添加单位,则使用像素进行计算。 但是,您可以明确设置您希望计算所基于的单位animate={{x: "20rem", y: "-20rem"}}>

跳跃后更多! 继续往下看↓

默认情况下,运动组件将从其样式定义的状态动画到animate道具中的状态。 但是,如果我们愿意,我们可以使用initial属性劫持并定义组件的初始动画状态。 虽然animate道具用于定义组件在安装时的行为,但initial道具在它们安装之前定义它们的行为。

如果我们希望我们的 h1 从左侧进入,我们使用初始属性来控制它。

 <motion.h1 initial={{x: -1000}} animate={{x: 20}}> This is a motion component </motion.h1>

现在,当h1挂载时,它会从左侧滑入。

我们不仅限于单个动画。 我们可以在一组值中定义一系列称为keyframes的动画。 每个值将按顺序进行动画处理。

 <motion.h1 initial={{x: -1000}} animate={{x: [20, 50, 0, -70, 40] }}> This is a motion component </motion.h1>

transition道具允许我们定义动画是如何发生的。 有了它,我们定义了值如何从一种状态变为另一种状态。 除此之外,我们可以使用这个道具定义动画的durationdelaytype

 <motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion component </motion.h1>

假设我们要同时为多个运动组件设置动画,如下面的代码片段所示。

 <div className="App"> <motion.h1 initial={{ x: -1000 }} animate={{ x: 0 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h1 </motion.h1> <motion.h2 initial={{ y: -1000 }} animate={{ y: 0 }} transition={{ type: "tween", duration: "1", delay: ".4" }}>This is a motion h2 </motion.h2> <motion.h3 initial={{ x: 100, opacity: 0 }} animate={{ x: 0, opacity: 1 }}> This is a motion h3 </motion.h3> <motion.h4 initial={{ scale: 0.7 }} animate={{ scale: 1.7 }} transition={{ type: "tween", duration: "2", delay: "1" }}> This is a motion h4 </motion.h4> </div>

虽然这可行,但 Framer Motion 中的 variables 属性使我们能够将动画定义提取到variants对象中。 variants不仅使我们的代码更清晰,而且使我们能够创建更强大和更复杂的动画。

将我们的动画定义提取到变体对象中,我们有:

 const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 } } const H2Variants = { initial: { y: -1000 }, animate: { y: 0 }, transition: { type: "tween", duration: 1, delay: .4 } } const H3Variants = { initial:{ x: 100, opacity: 0 }, animate:{ x: 0, opacity: 1 } } const H4Variants = { initial:{ scale: 0.7 }, animate:{ scale: 1.7 }, transition:{ type: "tween", duration: "2", delay: "1" } }

我们没有将动画定义直接传递到组件的initialanimate道具中,而是将这些定义提取到独立的变体对象中。 在变体对象中,我们定义了将每个动画名称描述为变体的变体名称。

 <div className="App"> <motion.h1 variants={H1Variants} initial='initial' animate='animate' > This is a motion h1 </motion.h1> <motion.h2 variants={H2Variants} initial='initial' animate='animate' > This is a motion h2 </motion.h2> <motion.h3 variants={H3Variants} initial='initial' animate='animate' > This is a motion h3 </motion.h3> <motion.h4 variants={H4Variants} initial='initial' animate='animate' > This is a motion h4 </motion.h4> </div>

variants道具中,我们为每个运动组件传入变体对象的名称,然后将动画传递给initial道具和animate道具。

我们可以将当前设置与变体一起进一步减少重复。 使用变体,我们可以通过 DOM 从父运动组件向下传播动画属性。 为此,我们为父motion.div创建变体,其变体对象中的动画名称与其子对象相似。 通过这样做,我们不必将动画名称传递给每个子组件。 在幕后,父元素为我们处理。

 const ContainerVariants = { initial: {}, animate: {} }; const H1Variants = { initial: { x: -1000 }, animate: { x: 0 }, transition: { type: "tween", duration: 2, delay: 1 } }; //more variants below <motion.div className="App" variants={ContainerVariants} initial="initial" animate="animate" > <motion.h1 variants={H1Variants}>This is a motion h1</motion.h1> <motion.h2 variants={H2Variants}>This is a motion h2</motion.h2> <motion.h3 variants={H3Variants}>This is a motion h3</motion.h3> <motion.h4 variants={H4Variants}>This is a motion h4</motion.h4> </motion.div>

现在我们有了一个没有重复的更简洁的代码。 我们将容器 div 转换为一个运动组件,这样我们就可以传入我们定义的ContainerVariants对象。 由于我们没有在容器上定义任何动画,我们将空对象传递给initialanimate 。 每个变体对象中的动画名称必须相同,才能使传播工作。

现在我们了解了 Framer Motion 的基础知识。 让我们开始构建我们的 5 个演示应用程序。

图标店

我们可以根据手势创建交互式动画。 运动组件目前能够侦听悬停、点击、平移和拖动手势检测。 我们将使用whileHover构建这个 Icon Shop 应用程序。

成分

  • App.js :这包含标题文本。
  • Card.jsx :在这里,我们定义了图标卡片的动画。
  • CardContainer.jsx :我们导入并循环遍历图标。
  • styles.js :创建、设置和导出运动组件。 我使用 styled-components 来设置组件的样式。

让我们从App.js开始。

 import { H1, H2 } from "./Styles"; import CardContainer from "./CardContainer"; return ( <div> <H1 initial={{ y: -100 }} animate={{ y: 0, transition: { delay: 1 } }}> Icon Shop </H1> <H2 initial={{ x: -1000 }} animate={{ x: 0, transition: { delay: 1 } }}> Hover over the cards to see the motion magic </H2> <CardContainer /> </div> );

我们导入在Styles.js文件中创建的H1H2运动组件。 由于它们是运动组件,我们使用initialanimate道具来定义它们在安装之前和安装时的行为。 在这里,我们还导入并显示CardContiner组件。

现在, CardContainer.js

 import { Container } from "./Styles"; import Card from "./Card"; import { ReactComponent as AddIcon } from "./assets/add.svg"; import { ReactComponent as AirplaneIcon } from "./assets/airplane.svg"; import { ReactComponent as AlarmIcon } from "./assets/alarm.svg"; //more svg imports below... const icons = [ <AddIcon />, <AirplaneIcon />, <AlarmIcon />, //more icons below ]; const CardContainer = () => { return ( <Container initial={{ x: -1000 }} animate={{ x: 0 }}> {icons.map((icon) => ( <Card icon={icon} /> ))} </Container> ); };

在这里,我们导入 SVG、 Container运动组件和Card组件。

App.js中的H1H2类似,我们使用initialanimate属性定义Container的动画。 当它加载时,它会产生一种从浏览器左侧滑入的酷炫效果。

现在, Card.js

 import { CardBox, IconBox } from "./Styles"; const CardVariants = { beforeHover: {}, onHover: { scale: 1.1 } }; const IconVariants = { beforeHover: { opacity: 0, y: -50 }, onHover: { opacity: 1, y: 0, scale: 1.5, transition: { type: "tween" } } }; const Card = ({ icon }) => { console.log(icon); return ( <CardBox variants={CardVariants} initial="beforeHover" whileHover="onHover"> <IconBox variants={IconVariants}>{icon}</IconBox> </CardBox> ); };

在这里,我们使用beforeHoveronHover动画创建了两个变体对象。 在CardVariants对象中,我们最初不想做任何事情,所以beforeHover是一个空对象。 onHover我们增加了卡片框的比例。

IconVariants对象中,我们在其beforeHover中定义了IconBox的初始状态。 我们将其不透明度设置为 0 并将其向上推 50 像素。 然后,在onHover中,我们将不透明度设置回 1,将其推回默认位置,并将过渡类型更改为tween 。 然后我们将变量传递给它们各自的运动分量。 我们利用传播,因此我们不需要显式地将initialanimate道具设置为IconBox组件。

动画导航栏

我们将构建一个简单的 Navigation 组件,并了解如何在父子运动组件之间创建时序关系。

成分

  • App.js :这包含标题文本。
  • Styles.js :创建、设置和导出运动组件。 组件使用 styled-components 设置样式。

让我们看一下App.js文件。

 import { Header, Nav, Link, SvgBox } from "./Styles"; function App() { const [isOpen, setIsOpen] = useState(false); const iconVariants = { opened: { rotate: 135 }, closed: { rotate: 0 } }; const menuVariants = { opened: { top: 0, transition: { when: "beforeChildren", staggerChildren: 0.5 } }, closed: { top: "-90vh" } }; const linkVariants = { opened: { opacity: 1, y: 50 }, closed: { opacity: 0, y: 0 } };

我们创建一个isOpen状态,用于检查导航栏是否打开。 我们创建了 3 个变体对象, iconVariantsmenuVariantslinkVariants ,我们分别在其中定义了SvgBoxNavLink运动组件的动画。 iconVariants用于在SvgBox悬停时将其旋转 135 度。 我们不需要在值中添加“deg”。 在menuVariants中,我们控制Nav的顶部位置,就像使用 CSS 中的position属性一样。 我们根据isOpen状态切换Nav的顶部位置。

通过变体,我们可以在父子运动组件之间创建时序关系。 我们使用转换对象中的when属性定义父Nav与其子之间的关系Link 。 在这里,将其设置为beforeChildren ,因此父组件的动画将在子动画开始之前完成。

使用staggerChildren属性,我们为每个链接设置时间顺序。 每个链接一个接一个出现需要 0.5 秒。 当Nav打开时,这会创建一个很好的视觉提示。 在linkVariants中,我们为每个链接的不透明度和垂直位置设置动画。

 <div className="App"> <Header> <SvgBox variants={iconVariants} animate={isOpen ? "opened" : "closed"} onClick={() => setIsOpen(!isOpen)} > <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="https://www.w3.org/2000/svg" > <path d="M12 4C11.4477 4 11 4.44772 11 5V11H5C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13H11V19C11 19.5523 11.4477 20 12 20C12.5523 20 13 19.5523 13 19V13H19C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11H13V5C13 4.44772 12.5523 4 12 4Z" fill="#fff" /> </svg> </SvgBox> </Header> <Nav initial={false} variants={menuVariants} animate={isOpen ? "opened" : "closed"} > <Link variants={linkVariants}>home</Link> <Link variants={linkVariants}>about</Link> <Link variants={linkVariants}>gallery</Link> </Nav> </div>

在这里,我们将变体传递给它们各自的组件。 在SvgBox中,只要单击它,我们就会切换isOpen的状态,然后根据状态有条件地对其进行动画处理。 与SvgBox一样,我们根据isOpen的状态有条件地为NavLink设置动画。

动画模态

我们将构建一个模态组件并了解 Framer Motion 的AnimatePresence ,以及它如何让我们在元素离开 DOM 时对其进行动画处理。

成分:

  • App.js :我们在这里设置了showModal状态。
  • Modal.jsx :实际的动画工作发生在这里。
  • Styles.js :创建、设置和导出运动组件。 组件使用 styled-components 设置样式。

让我们看看App.js

 import { ToggleButton, Container } from "./Styles"; import Modal from "./Modal"; function App() { const [showModal, setShowModal] = useState(false); const toggleModal = () => { setShowModal(!showModal); }; return ( <Container> <ToggleButton initial={{ x: -700 }} animate={{ x: 0, transition: { duration: 0.5 } }} onClick={toggleModal} > Toggle Modal </ToggleButton> <Modal showModal={showModal} /> </Container> ); }

我们创建一个showModal状态,用于有条件地渲染模态。 每当单击ToggleButton时, toggleModal函数都会切换状态。 ToggleButton是一个运动组件,所以我们可以为它定义动画。 安装时,它会从左侧滑入。 此动画运行 0.5 秒。 我们还通过 props 将showModal状态传递给Modal

现在, Modal.jsx

 import { AnimatePresence } from "framer-motion"; import { ModalBox, ModalContent, Container } from "./Styles"; <Container> <AnimatePresence> {showModal && ( <ModalBox initial={{ opacity: 0, y: 60, scale: 0.3 }} animate={{ opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 300 } }} exit={{ opacity: 0, scale: 0.5, transition: { duration: 0.6 } }} > <ModalContent initial={{ y: -30, opacity: 0 }} animate={{ y: 0, opacity: 1, transition: { delay: 1 } }} > Modal content!!!! </ModalContent> </ModalBox> )} </AnimatePresence> </Container>

我们从framer-motion导入AnimatePresence 。 它允许我们在组件离开 DOM 时为它们设置退出动画。 我们根据showModal状态有条件地渲染Modal 。 我们通过它们的initialanimate道具为ModalBoxModalContent定义动画。 这里还有一个新道具, exitAnimatePresence作为包装器允许我们将退出动画添加到exit道具中的ModalBox

滚动动画

我们将结合使用useAnimation钩子和react-intersection-observer来创建滚动触发的动画。

成分

  • App.js :我们为Box组件设置动画并在App中渲染它
  • Styles.js :创建、设置和导出运动组件。 组件使用 styled-components 设置样式。
 import React, { useEffect } from "react"; import { useAnimation } from "framer-motion"; import { useInView } from "react-intersection-observer"; import { Container, H1,StyledBox } from "./Styles"; const BoxVariants = { visible: { opacity: 1, x: 0, transition: { duration: 1 } }, hidden: { opacity: 0, x: 300 }, }; const Box = () => { const controls = useAnimation(); const [ref, inView] = useInView(); useEffect(() => { if (inView) { controls.start("visible"); } }, [controls, inView]); return ( <StyledBox ref={ref} animate={controls} initial="hidden" variants={BoxVariants} /> ); };

useAnimation钩子允许我们控制动画发生的顺序。 我们可以使用controls.startcontrols.stop方法来手动启动和停止动画。 我们将初始hidden动画传递给StyledBox 。 我们将使用start方法定义的控件传递给StyledBox animate prop。

react-intersection-observeruseInView钩子允许我们跟踪组件何时在视口中可见。 useInView钩子让我们可以访问ref ,我们将其传递给我们想要观看的组件,以及inView布尔值,它告诉我们该元素是否是inView 。 每当我们正在观看的元素StyledBox在视图中时,我们就使用useEffect调用controls.start 。 我们将controlsinView作为useEffect的依赖项传入。 此外,我们将我们定义的变体BoxVariantsStyledBox

英雄动画

我们将使用useCycle钩子构建一个很酷的英雄横幅动画。 我们将了解useCycle如何让我们循环播放动画。

 import React, { useEffect } from "react"; import { useCycle } from "framer-motion"; import { Container, H1, HeroSection, Banner, TextBox } from "./Styles"; import { ReactComponent as BannerIllustration } from "./bighead.svg"; const H1Variants = { initial: { y: -200, opacity: 0 }, animate: { y: 0, opacity: 1, transition: { delay: 1 } }, }; const TextVariants = { initial: { x: 400 }, animate: { x: 0, transition: { duration: 0.5 } }, }; const BannerVariants = { animationOne: { x: -250, opacity: 1, transition: { duration: 0.5 } }, animationTwo: { y: [0, -20], opacity: 1, transition: { yoyo: Infinity, ease: "easeIn" }, }, };

我们定义了 3 个变体, H1VariantsTextVariantsBannerVariants 。 但是,我们的重点是BannerVariants 。 我们在BannerVariants中定义了 2 个动画, animationOneanimationTwo 。 这些是我们传递给useCycle循环的动画。

 const [animation, cycleAnimation] = useCycle("animationOne", "animationTwo"); useEffect(() => { setTimeout(() => { cycleAnimation(); }, 2000); }, []);

useCycle的工作方式类似于useState挂钩。 在解构数组中, animation表示处于活动状态的动画,无论是animationOne还是animationTwo 。 在我们定义的动画之间循环的cylceAnimation函数。 我们将想要循环的动画传入useCycle并在 2 秒后调用cylceAnimationuseEffect中。

 <div className="App"> <Container> <H1 variants={H1Variants} initial="initial" animate="animate"> Cool Hero Section Anmiation </H1> <HeroSection> <TextBox variants={TextVariants} initial="initial" animate="animate"> Storage shed, troughs feed bale manure, is garden wheat oats at augers. Bulls at rose garden cucumbers mice sunflower wheat in pig. Chainsaw foal hay hook, herbs at combine harvester, children is mallet. Goat goose hen horse. Pick up truck livestock, pets and storage shed, troughs feed bale manure, is garden wheat oats at augers. Lamb. </TextBox> <Banner variants={BannerVariants} animate={animation}> <BannerIllustration /> </Banner> </HeroSection> </Container> </div>

在一切结束时,我们将变体传递给它们各自的组件并观察奇迹发生。 这样一来, Banner将根据我们在 animationOne 中定义的animationOne从右侧滑入,2 秒后,将调用cycleAnimation触发animationTwo

正如一只聪明的猪曾经说过的那样,“这就是所有人。”

结论

我们已经了解了 Framer Motion 的基础知识,并查看了一些演示项目,这些项目让我们了解了我们可以创建的动画范围。 但是,您可以用它做更多的事情。 我鼓励您深入研究文档并疯狂。

资源

  • Framer Motion Api 文档,Framer Motion
  • react-intersection-observer,npm
  • 用于 React、NetNin​​ja 的 Framer Motion