使用@react spring/web进行动画倒计时

目录
文章目录隐藏
  1. 使用到技术
  2. 如何实现
  3. 创建<Box/ >组件
  4. 创建 <Countdown /> 组件
  5. 创建一个帮助我们进行分解的函数
  6. 混合之前的所有点

今天,我把我在 react 中开发动画倒计时的步骤分享给大家。

使用到技术

  1. vite
  2. @react-spring/web
  3. date-fns
  4. styled-components

如何实现

由于我们想“创建一个倒计时,用来呈现从未来某个日期开始的剩余时间”,主要步骤:

  1. 创建一个抽象的<Box/>组件,该组件只负责渲染内部的标签,并在每次标签更改时为其自身设置动画。在这个 demo 中,我们可以找到 4 个<Box/>
  2. 创建<Countdown/>组件,用来负责渲染 4 个<Box/>组件标签。
  3. 创建一个函数,帮助我们从给定的未来日期开始计算一天、几小时、几分钟和几秒。
  4. 混合之前的所有点

这个 demo 一些地方用到了 css 盒子模型,所以我们在 index.css 中写入:

* {
  box-sizing: border-box!important;
}

创建<Box/ >组件

正如我之前所说的,我们想要构建一个可重用的组件。此组件只需要两个 props:

  1. labelPeriod(string):我们将通过“day”、“hours”等。但我们也可以通过“day ramaining”或“hours remaining”或“days of waiting”
  2. labelNumber(number):这将是一个来自 helper 函数的值。每次此值更改时,动画都会开始。

由于我们想要一个覆盖当前数字并显示下一个数字的动画,我们需要在这个组件上创建两个不同的面,并使用一个非常酷的 css 实用属性,称为transform-style: preserve-3d。由于此特性与transform: rotateX(180deg)相结合,我们可以有两个辐射面,其中背面旋转并被主面覆盖。

创建<Box/ >组件

这里展示第一个实现:

export const Box: React.FC<Props> = ({ labelNumber, labelPeriod }) => {
  return (
    <Container>
      <Card className="card">
        <Content>
          <ContentFront>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentFront>
          <ContentBack>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentBack>
        </Content>
      </Card>
    </Container>
  );
};

动画使用@react-spring/web 里useSpring()函数。

const [style,api] = useSpring(
    () => ({
      from: { rotateX: "0deg" },
      to: { rotateX: "180deg" },
      delay: 0,
      reset: true,
    }),
    [labelNumber]
  );

这个钩子返回两个字段:

  1. 第一个样式:这个对象是一个基本样式对象,我们可以将其传递给<animated.div/>元素(由@react-spring/web 提供)。
  2. 第二个 api:在这个演示中没有使用,但钩子还返回一个对象,您可以在命令模式下调用api.start()

像普通钩子一样,我们添加[labelNumber]作为依赖项。通过这种方式,动画取决于labelNumber。因此,当 labelNumber 更改时,动画将重新开始。

from 和 to 需要是相同的类型。所以rotateX需要与内部的rotateX类型相同。在这个演示中,我们使用了一个字符串值。混合整数(如rotateX:0)和字符串不起作用。

当我们将样式传递到animated.div中时,神奇的事情就会发生。由于我们使用样式组件来添加基本样式,因此需要创建这样的组件:

import styled from "styled-components";
import { animated } from "@react-spring/web";

const Content = styled(animated.div)`
  position: absolute;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  perspective: 500px;
`;

然后将样式添加到<Content/>组件中:

<Container>
  <Card className="card">
    <Content style={style}>
          ....
    </Content>
  </Card>
</Container>

在那之后,如果我们设置一个这样的基本容器:

import useTimer from "./hooks/useTimer";
import { Box } from "./components/card2";

export default function App() {
  // use timer start a timer
  // seconds will be update each 1000ms
  const { seconds } = useTimer(1000, () => {});

  return (
    <main>
      <div
        style={{
          width: 400,
        }}
      >
        <Box labelPeriod="day" labelNumber={seconds} />
      </div>
    </main>
  );
}

我们有了第一部动画:

第一部动画

效果是有了,但我们有一个小错误。当标签更改时,动画会正确开始,但由于反应渲染很快,即使动画没有结束,用户也可以看到新的数字。之所以会发生这种情况,是因为在<ContentBack/>中,我们呈现了与<ContentFront/>相同的值。我们可以使用名为usePrevious()的自定义钩子来避免这种行为。这个钩子(这里的源代码)向我们返回给定值的前一个值。

添加usePrevious()后,我们编辑代码并将其传递到<ContentBack/>

const previous = usePrevious(labelNumber);

  return (
    <Container>
      <Card className="card">
        <Content style={props}>
          <ContentFront>
            <span>{labelNumber}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentFront>
          <ContentBack>
            <span>{previous}</span>
            <LabelPeriod>{labelPeriod}</LabelPeriod>
          </ContentBack>
        </Content>
      </Card>
    </Container>
  );

结果将与演示链接相同(但仅适用于一个<Box/>)。

创建 <Countdown /> 组件

这只是一个基本的包装。由于我们有dayhoursminutesseconds,我们设置了这样一个组件:

export default function Countdown({ startDate, endDate }: Props) {
  // We will discuss this works later
  const { days, hours, minutes, seconds } = getRemamingRangeTimes(
    startDate,
    endDate
  );

  return (
    <Container>
      <Box labelNumber={days} labelPeriod="days" />
      <Box labelNumber={hours} labelPeriod="hours" />
      <Box labelNumber={minutes} labelPeriod="minutes" />
      <Box labelNumber={seconds} labelPeriod="seconds" />
    </Container>
  );
}

创建一个帮助我们进行分解的函数

这里推荐一个名为 Date-dns 的库,可以使我们从未来的日期中获取dayshours等。也许我们可以用window.Date做同样的计算。虽然工作量大,但这个是必须要做的。

import {
  addDays,
  differenceInDays,
  differenceInHours,
  differenceInMinutes,
  differenceInSeconds,
} from "date-fns";

export function generateFutureDay() {
  const future = addDays(new Date(), 10);
  return future;
}

export function getRemamingRangeTimes(startDate: Date, endDate: Date) {
  const days = Math.max(0, differenceInDays(endDate, startDate));
  const hours = Math.max(0, differenceInHours(endDate, startDate) % 24);
  const minutes = Math.max(0, differenceInMinutes(endDate, startDate) % 60);
  const seconds = Math.max(0, differenceInSeconds(endDate, startDate) % 60);

  return {
    days,
    hours,
    minutes,
    seconds,
  };
}

getRemamingRangeTimes函数是核心。我们使用Math.max只是因为,如果由于某些原因,我们将过去的日期作为endDate传递,我们不希望倒计时呈现负数。

由于differenceInMinutesdifferenceInHoursdifferenceInSeconds返回的时间相同,因此我们仅使用%24来计算最后一天的剩余小时数。几分钟内逻辑相同。

混合之前的所有点

由于我们想每秒钟触发一次动画,我们需要基于Date.now()来“生成”标签数。所以我们可以写:

import { generateFutureDay } from "./utils";
import useTimer from "./hooks/useTimer";
import { useRef, useState } from "react";
import Countdown from "./components/countdown";

export default function App() {
  const futureDate = useRef(generateFutureDay());
  const [now, setNow] = useState(new Date());

  const {} = useTimer(1000, () => {
    // refresh now
    setNow(new Date());
  });

  return (
    <main>
      <Countdown startDate={now} endDate={futureDate.current} />
    </main>
  );
}

最终结果:

使用@react spring/web 进行动画倒计时

演示: 点击这里
全部源码:点击这里

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » 使用@react spring/web进行动画倒计时

发表回复