AnimateNumber – React 数字动画组件
AnimateNumber
是一个优雅的 React 数字动画组件,提供流畅的数字过渡效果。它支持单个数字动画和数字组动画,适用于数据展示、计数器、统计数据等场景。
✨ 特性
- 🎯 支持单个数字和多位数字的动画效果
- 🔄 支持向上和向下的动画方向
- ⏱️ 可自定义动画持续时间
- ⏳ 支持动画延迟
- 🎨 完全可定制的样式
- 🔥 基于 Framer Motion 的流畅动画
- 📱 响应式设计
组件源码
import { motion, AnimatePresence } from "framer-motion";
import { useMemo, useEffect, useState } from "react";
export default function AnimateNumber({
initial = 0,
value = 0,
className = "",
containerClassName = "",
duration = 0.5,
delay = 0,
direction = "up",
isTransition = false,
}) {
return (
<>
{isTransition ? (
<DigitGroup
from={initial}
to={value}
value={value}
className={className}
duration={duration}
containerClassName={containerClassName}
delay={delay}
/>
) : (
<Digit
value={value}
className={className}
duration={duration}
direction={direction}
/>
)}
</>
);
}
const DigitGroup = ({
from = 0,
to = 0,
className = "",
containerClassName = "",
duration = 0.5,
delay = 0,
}) => {
const [count, setCount] = useState(from);
const difference = to - from;
const direction = to > from ? "up" : "down";
useEffect(() => {
// 如果起始值和目标值相同,不执行动画
if (from === to) return;
// 动画开始的时间
let startTime;
// 动画帧ID
let animationFrameId;
// 动画函数
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
// 计算动画已经进行的时间(考虑延迟)
const elapsed = timestamp - startTime - delay * 1000;
if (elapsed < 0) {
// 如果还在延迟中,继续等待
animationFrameId = requestAnimationFrame(animate);
return;
}
// 计算动画进度(0-1之间)
const progress = Math.min(elapsed / (duration * 1000), 1);
// 计算当前值
const currentValue = Math.round(from + difference * progress);
// 更新状态
setCount(currentValue);
// 如果动画未完成,继续下一帧
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate);
}
};
// 开始动画
animationFrameId = requestAnimationFrame(animate);
// 清理函数
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [from, to, duration, delay, difference]);
const digits = String(count).split("");
return (
<div className={`flex items-center gap-2 ${containerClassName}`}>
{digits.map((digit, index) => (
<Digit
key={index}
value={digit}
duration={duration}
direction={direction}
className={className}
/>
))}
</div>
);
};
/**
* 数字动画组件
* @param {Object} props
* @param {number} props.value - 数字值
* @param {string} props.className - 额外的样式类
* @param {number} props.duration - 动画持续时间
* @param {string} props.direction - 动画方向
*/
export const Digit = ({
value = 0,
className = "",
duration = 0.5,
direction = "up",
}) => {
const variants = useMemo(() => {
return {
enter: { y: direction === "up" ? "100%" : "-100%", opacity: 0 },
center: { y: 0, opacity: 1 },
exit: { y: direction === "up" ? "-100%" : "100%", opacity: 0 },
};
}, [direction]);
return (
<div className={`animate-number relative overflow-hidden ${className}`}>
<AnimatePresence mode="popLayout">
<motion.div
key={value}
initial="enter"
animate="center"
exit="exit"
variants={variants}
transition={{
y: { type: "spring", stiffness: 200, damping: 25, duration },
}}
>
{value}
</motion.div>
</AnimatePresence>
</div>
);
};
📝 API
属性
属性名 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | number | 0 | 要显示的数值 |
initial | number | 0 | 初始数值(仅在isTransition 为true 时使用) |
duration | number | 0.5 | 动画持续时间(秒) |
delay | number | 0 | 动画延迟时间(秒) |
direction | 'up' | 'down' | 'up' | 动画方向 |
isTransition | boolean | false | 是否启用数字组过渡动画 |
className | string | '' | 数字容器的自定义类名 |
containerClassName | string | '' | 外层容器的自定义类名 |
🌰 使用示例
简单计数器
import AnimateNumber from "./AnimateNumber";
import { useState, useCallback } from "react";
export default function Default() {
const [value, setValue] = useState(0);
const [direction, setDirection] = useState("up");
const decrement = useCallback(() => {
if (value === 0) return;
setValue(value - 1);
setDirection("down");
}, [value]);
const increment = useCallback(() => {
setValue(value + 1);
setDirection("up");
}, [value]);
return (
<>
<div className="flex items-center gap-2">
<button
onClick={decrement}
className="w-8 h-8 rounded-md bg-gray-900 hover:bg-gray-700 text-white text-sm font-medium transition-colors duration-200 focus:outline-none focus:ring-1 focus:ring-gray-400"
>
-
</button>
<AnimateNumber
value={value}
direction={direction}
className="min-w-[3rem] text-lg font-medium bg-gray-100 rounded-md p-1 text-center"
/>
<button
onClick={increment}
className="w-8 h-8 rounded-md bg-gray-900 hover:bg-gray-700 text-white text-sm font-medium transition-colors duration-200 focus:outline-none focus:ring-1 focus:ring-gray-400"
>
+
</button>
</div>
</>
);
}

数据统计展示
import AnimateNumber from "./AnimateNumber";
import { useState, useCallback } from "react";
export default function Default() {
const [randomValue, setRandomValue] = useState(0);
const random = useCallback(() => {
setRandomValue(Math.floor(Math.random() * 1000));
}, []);
return (
<>
<div className="flex items-center gap-2">
<AnimateNumber
isTransition={true}
initial={0}
value={randomValue}
duration={0.3}
direction="up"
className="min-w-[3rem] text-lg font-medium bg-gray-100 rounded-md p-1 text-center"
/>
<button
onClick={random}
className="h-8 rounded-md bg-gray-900 hover:bg-gray-700 text-white text-sm font-medium transition-colors duration-200 focus:outline-none focus:ring-1 focus:ring-gray-400 px-2"
>
Random
</button>
</div>
</>
);
}

🎨 样式定制
组件支持通过className
和containerClassName
属性进行样式定制:
<AnimateNumber
value={50}
className="text-2xl font-bold text-blue-600"
containerClassName="bg-gray-100 p-4 rounded-lg shadow"
/>
🚀 最佳实践
- 数据展示:
- 用于展示不断更新的数据
- 适合展示统计数字、计数器等
- 用户交互:
- 在数值变化时提供视觉反馈
- 让数据变化更加生动直观
- 性能优化:
- 对于大数值变化,建议使用
isTransition
模式 - 可以通过
duration
控制动画速度
🔧 注意事项
- 大数值变化时建议使用
isTransition
模式以获得更好的动画效果 - 动画持续时间(
duration
)建议根据实际场景调整,过快或过慢都可能影响用户体验 - 确保提供适当的
className
以保持组件与整体设计风格的一致性
🛠️ 技术栈
- React
- Framer Motion
原文链接:https://code.ifrontend.net/archives/408,转载请注明出处。
评论0