使用@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进行动画倒计时