使用@react spring/web进行动画倒计时
今天,我把我在 react 中开发动画倒计时的步骤分享给大家。
使用到技术
- vite
- @react-spring/web
- date-fns
- styled-components
如何实现
由于我们想“创建一个倒计时,用来呈现从未来某个日期开始的剩余时间”,主要步骤:
- 创建一个抽象的
<Box/>组件,该组件只负责渲染内部的标签,并在每次标签更改时为其自身设置动画。在这个 demo 中,我们可以找到 4 个<Box/>。 - 创建
<Countdown/>组件,用来负责渲染 4 个<Box/>组件标签。 - 创建一个函数,帮助我们从给定的未来日期开始计算一天、几小时、几分钟和几秒。
- 混合之前的所有点
这个 demo 一些地方用到了 css 盒子模型,所以我们在 index.css 中写入:
* {
box-sizing: border-box!important;
}
创建<Box/ >组件
正如我之前所说的,我们想要构建一个可重用的组件。此组件只需要两个 props:
- labelPeriod(string):我们将通过“day”、“hours”等。但我们也可以通过“day ramaining”或“hours remaining”或“days of waiting”
- labelNumber(number):这将是一个来自 helper 函数的值。每次此值更改时,动画都会开始。
由于我们想要一个覆盖当前数字并显示下一个数字的动画,我们需要在这个组件上创建两个不同的面,并使用一个非常酷的 css 实用属性,称为transform-style: preserve-3d。由于此特性与transform: rotateX(180deg)相结合,我们可以有两个辐射面,其中背面旋转并被主面覆盖。

这里展示第一个实现:
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]
);
这个钩子返回两个字段:
- 第一个样式:这个对象是一个基本样式对象,我们可以将其传递给
<animated.div/>元素(由@react-spring/web 提供)。 - 第二个 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 /> 组件
这只是一个基本的包装。由于我们有day, hours, minutes和seconds,我们设置了这样一个组件:
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 的库,可以使我们从未来的日期中获取days 、hours等。也许我们可以用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传递,我们不希望倒计时呈现负数。
由于differenceInMinutes、differenceInHours和differenceInSeconds返回的时间相同,因此我们仅使用%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进行动画倒计时的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » 使用@react spring/web进行动画倒计时
微信
支付宝