所有分类
  • 所有分类
  • Html5资源
  • React资源
  • Vue资源
  • Php资源
  • ‌小程序资源
  • Python资源

Motion — React数字动画组件实现 AnimateNumber

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

属性

属性名类型默认值说明
valuenumber0要显示的数值
initialnumber0初始数值(仅在isTransitiontrue时使用)
durationnumber0.5动画持续时间(秒)
delaynumber0动画延迟时间(秒)
direction'up' | 'down''up'动画方向
isTransitionbooleanfalse是否启用数字组过渡动画
classNamestring''数字容器的自定义类名
containerClassNamestring''外层容器的自定义类名

🌰 使用示例

简单计数器

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>
    </>
  );
}

🎨 样式定制

组件支持通过classNamecontainerClassName属性进行样式定制:

<AnimateNumber
  value={50}
  className="text-2xl font-bold text-blue-600"
  containerClassName="bg-gray-100 p-4 rounded-lg shadow"
/>

🚀 最佳实践

  1. 数据展示
  • 用于展示不断更新的数据
  • 适合展示统计数字、计数器等
  1. 用户交互
  • 在数值变化时提供视觉反馈
  • 让数据变化更加生动直观
  1. 性能优化
  • 对于大数值变化,建议使用isTransition模式
  • 可以通过duration控制动画速度

🔧 注意事项

  1. 大数值变化时建议使用isTransition模式以获得更好的动画效果
  2. 动画持续时间(duration)建议根据实际场景调整,过快或过慢都可能影响用户体验
  3. 确保提供适当的className以保持组件与整体设计风格的一致性

🛠️ 技术栈

  • React
  • Framer Motion
原文链接:https://code.ifrontend.net/archives/408,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?