当前位置: 首页 > news >正文

[React] 手动实现CountTo 数字滚动效果

这个CountTo组件npmjs里当然有大把的依赖存在,不过今天我们不需要借助任何三方依赖,造个轮子来手动实现这个组件。

通过研究其他count to插件我们可以发现,数字滚动效果主要依赖于requestAnimationFrame 通过js帧来让数字动起来,数字变化则是依赖于内部的easingFn函数来每次计算。

首先声明组件props类型

interface Props {/*** 动画开始的值*/start?: number;/*** 目标值*/end: number;/*** 持续时间*/duration?: number;/*** 是否自动播放*/autoPlay?: boolean;/*** 精度*/decimals?: number;/*** 小数点*/decimal?: string;/*** 千分位分隔符*/separator?: string;/*** 数字前 额外信息*/prefix?: string;/*** 数字后 额外信息*/suffix?: string;/*** 是否使用变速函数*/useEasing?: boolean;/*** 计算函数*/easingFn?: (t: number, b: number, c: number, d: number) => number;/*** 动画开始后传给父组件的回调*/started?: () => void;/*** 动画结束传递给父组件的回调*/ended?: () => void;
}

除了end 是必要的,其他都是可选参数。
所以我们需要给组件默认值,防止没有参数时会报错。
同时写几个工具函数便于后面使用

export default function Index({end,start = 0,duration = 3000,autoPlay = true,decimals = 0,decimal = '.',separator = ',',prefix = '',suffix = '',useEasing = true,easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,started = () => {},ended = () => {},
}: Props) {const isNumber = (val: string) => {return !isNaN(parseFloat(val));};// 格式化数据,返回想要展示的数据格式const formatNumber = (n: number) => {let val = '';if (n % 1 !== 0) val = n.toFixed(decimals);const x = val.split('.');let x1 = x[0];const x2 = x.length > 1 ? decimal + x[1] : '';const rgx = /(\d+)(\d{3})/;if (separator && !isNumber(separator)) {while (rgx.test(x1)) {x1 = x1.replace(rgx, '$1' + separator + '$2');}}return prefix + x1 + x2 + suffix;};...
}

初始化数据

  const [state, setState] = useState<State>({start: 0,paused: false,duration,});const startTime = useRef(0);const _timestamp = useRef(0);const remaining = useRef(0);const printVal = useRef(0);const rAf = useRef(0);const endRef = useRef(end);const endedCallback = useRef(ended);const [displayValue, setValue] = useState(formatNumber(start));// 定义一个计算属性,当开始数字大于结束数字时返回trueconst stopCount = useMemo(() => start > end, [start, end]);

动画的关键函数

  const count = (timestamp: number) => {if (!startTime.current) startTime.current = timestamp;_timestamp.current = timestamp;const progress = timestamp - startTime.current;remaining.current = state.duration - progress;// 是否使用速度变化曲线if (useEasing) {if (stopCount) {printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);} else {printVal.current = easingFn(progress, state.start, end - state.start, state.duration);}} else {if (stopCount) {printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);} else {printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);}}if (stopCount) {printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;} else {printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;}setValue(formatNumber(printVal.current));if (progress < state.duration) {rAf.current = requestAnimationFrame(count);} else {endedCallback.current?.();}};

执行动画的函数

  const startCount = () => {setState({ ...state, start, duration, paused: false });rAf.current = requestAnimationFrame(count);startTime.current = 0;};

挂载时监听是否有autoPlay 来选择是否开始动画,同时组件销毁后清除requestAnimationFrame动画;

  useEffect(() => {if (autoPlay) {startCount();started?.();}return () => {cancelAnimationFrame(rAf.current);};}, []);

一些相关依赖的监听及处理

useEffect(() => {if (!autoPlay) {cancelAnimationFrame(rAf.current);setState({ ...state, paused: true });}}, [autoPlay]);useEffect(() => {if (!state.paused) {cancelAnimationFrame(rAf.current);startCount();}}, [start]);

最后返回displayValue就可以了;

好了 我要开启五一假期了!
最后附上完整代码 –

'use client';import { useEffect, useMemo, useRef, useState } from 'react';interface Props {/*** 动画开始的值*/start?: number;/*** 目标值*/end: number;/*** 持续时间*/duration?: number;/*** 是否自动播放*/autoPlay?: boolean;/*** 精度*/decimals?: number;/*** 小数点*/decimal?: string;/*** 千分位分隔符*/separator?: string;/*** 数字前 额外信息*/prefix?: string;/*** 数字后 额外信息*/suffix?: string;/*** 是否使用变速函数*/useEasing?: boolean;/*** 计算函数*/easingFn?: (t: number, b: number, c: number, d: number) => number;/*** 动画开始后传给父组件的回调*/started?: () => void;/*** 动画结束传递给父组件的回调*/ended?: () => void;
}
interface State {start: number;paused: boolean;duration: number;
}
export default function Index({end,start = 0,duration = 3000,autoPlay = true,decimals = 0,decimal = '.',separator = ',',prefix = '',suffix = '',useEasing = true,easingFn = (t, b, c, d) => (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b,started = () => {},ended = () => {},
}: Props) {const isNumber = (val: string) => {return !isNaN(parseFloat(val));};// 格式化数据,返回想要展示的数据格式const formatNumber = (n: number) => {let val = '';if (n % 1 !== 0) val = n.toFixed(decimals);const x = val.split('.');let x1 = x[0];const x2 = x.length > 1 ? decimal + x[1] : '';const rgx = /(\d+)(\d{3})/;if (separator && !isNumber(separator)) {while (rgx.test(x1)) {x1 = x1.replace(rgx, '$1' + separator + '$2');}}return prefix + x1 + x2 + suffix;};const [state, setState] = useState<State>({start: 0,paused: false,duration,});const startTime = useRef(0);const _timestamp = useRef(0);const remaining = useRef(0);const printVal = useRef(0);const rAf = useRef(0);const endRef = useRef(end);const endedCallback = useRef(ended);const [displayValue, setValue] = useState(formatNumber(start));// 定义一个计算属性,当开始数字大于结束数字时返回trueconst stopCount = useMemo(() => start > end, [start, end]);const count = (timestamp: number) => {if (!startTime.current) startTime.current = timestamp;_timestamp.current = timestamp;const progress = timestamp - startTime.current;remaining.current = state.duration - progress;// 是否使用速度变化曲线if (useEasing) {if (stopCount) {printVal.current = state.start - easingFn(progress, 0, state.start - end, state.duration);} else {printVal.current = easingFn(progress, state.start, end - state.start, state.duration);}} else {if (stopCount) {printVal.current = state.start - (state.start - endRef.current) * (progress / state.duration);} else {printVal.current = state.start + (endRef.current - state.start) * (progress / state.duration);}}if (stopCount) {printVal.current = printVal.current < endRef.current ? endRef.current : printVal.current;} else {printVal.current = printVal.current > endRef.current ? endRef.current : printVal.current;}setValue(formatNumber(printVal.current));if (progress < state.duration) {rAf.current = requestAnimationFrame(count);} else {endedCallback.current?.();}};const startCount = () => {setState({ ...state, start, duration, paused: false });rAf.current = requestAnimationFrame(count);startTime.current = 0;};useEffect(() => {if (!autoPlay) {cancelAnimationFrame(rAf.current);setState({ ...state, paused: true });}}, [autoPlay]);useEffect(() => {if (!state.paused) {cancelAnimationFrame(rAf.current);startCount();}}, [start]);useEffect(() => {if (autoPlay) {startCount();started?.();}return () => {cancelAnimationFrame(rAf.current);};}, []);return displayValue;
}
http://www.lryc.cn/news/341833.html

相关文章:

  • 9.Admin后台系统
  • redis之集群
  • #9松桑前端后花园周刊-React19beta、TS5.5beta、Node22.1.0、const滥用、jsDelivr、douyin-vue
  • STM32中UART通信的完整C语言代码范例
  • 【ITK统计】第一期 分类器
  • 51单片机两个中断及中断嵌套
  • VUE 监视数据原理
  • Thinkphp使用dd()函数
  • Git使用指北
  • STM32G030F6P6TR 芯片TSSOP20 MCU单片机微控制器芯片
  • 零基础入门学习Python第二阶01生成式(推导式),数据结构
  • Java面试题:多线程3
  • 【QEMU系统分析之实例篇(十八)】
  • pyside6的调色板QPalette的简单应用
  • 苍穹外卖项目
  • error: Execution was interrupted, reason: signal SIGABRT
  • HarmaonyOS鸿蒙应用科普课
  • 数码管的显示
  • 关于海康相机和镜头参数的记录
  • 【JavaScript】运算符
  • LabVIEW航空发动机主轴承试验器数据采集与监测
  • CVE-2022-2602:unix_gc 错误释放 io_uring 注册的文件从而导致的 file UAF
  • LSTM实战笔记(部署到C++上)——更新中
  • 鸿蒙内核源码分析(消息队列篇) | 进程间如何异步传递大数据
  • Sentinel流量防卫兵
  • 微信小程序:14.什么是wxs,wxs的使用
  • Django运行不提示网址问题
  • web安全---xss漏洞/beef-xss基本使用
  • 第一天学习(GPT)
  • 【C++之AVL树旋转操作的详细图解】