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