如何实现一个数字滚动组件

1,918 阅读2分钟

背景

在做营销活动时候,比如抽奖得红包/现金环节,经常需要数字从一个数值滚动变化到另一个数值,可以增加活动的参与感,现金从1块钱滚动到100块,参与活动人数从100个变成1000个,奖池剩余奖品数从50个变成1个,都是这个组件应用的场景,这里总结下如何实现这个组件

组件

属性定义

export interface ScrollNumberProps {
  // 数字滚动动画时间
  period: number
  // 起始值
  from: number
  // 结束值
  to: number
  // 控制滚动是否开始
  autoScroll: boolean
}

原理

  • 动画开始,记录开始动画的时间 startTimeRef.current
    const startTimeRef = useRef(Date.now());
    const [t, setT] = useState(Date.now());
    
  • 之后每一帧动画,记录从开始动画经过了多长时间,计算出当前帧的所到达的数字应该是多少 - currentValue
    useEffect(() => {
        const rafFunc = () => {
          const now = Date.now();
          const t = now - startTimeRef.current;
          if (t >= period) {
            setT(period);
          } else {
            setT(t);
            requestAnimationFrame(rafFunc);
          }
        };
        let raf;
        if (autoScroll) {
          raf = requestAnimationFrame(rafFunc);
          startTimeRef.current = Date.now();
        } else {
          raf && cancelAnimationFrame(raf);
        }
        return () => raf && cancelAnimationFrame(raf);
    }, [period, autoScroll]);
    
    const currentValue = useMemo(() => ((to - from) / period) * t + from, [t, period, from, to]);
    
  • 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画;
    • 总的数字位的计算,tofrom 最大者的长度
    const nlen = useMemo(() => Math.ceil(Math.log(Math.max(from, to) + 1) / Math.log(10)), [from, to]);
    
  • 个位数字上的偏移变化会导致下一位的变化,所以考虑所有数字位上的变化其实就是考虑个位数字上的数值变化;
  • 渲染每个数字位上的数字;
    <div className="scroll-number-list">
      {Array.from(new Array(nlen), (_, idx) => {
        // 遍历每个数字位
        const nth = nlen - idx - 1;
        // 计算当前动画帧的结束值
        const currentNthAnimationEndValue =
          (Math.floor(currentValue / Math.pow(10, nth)) + 1) *
          Math.pow(10, nth);
        // 计算当前动画帧的起始值 每一帧的偏移量为1 起始值到结束值为一个区间
        const currentNthAnimationStartValue = currentNthAnimationEndValue - 1;
        let currentNthAnimationOffset = 0;
        let currentShowValue = currentValue;
        // 如果当前值落在当前区间内,说明当前帧该数字位上存在动画,需要计算偏移量,否则数字位静止不动
        if (
          currentNthAnimationEndValue > currentValue &&
          currentValue > currentNthAnimationStartValue
        ) {
          currentNthAnimationOffset =
            currentNthAnimationEndValue - currentValue;
          currentShowValue = currentNthAnimationEndValue;
        }
        const currentNthShowValue = currentShowValue % Math.pow(10, nth + 1);
        const currentNthNormShowValue = currentNthShowValue / Math.pow(10, nth);
        const showNum = Math.floor(currentNthNormShowValue);
        // 同一个数字位上设置前后两个div是因为视觉看到的数字滚动效果在当前数字位容器内最多看到两个连续的,因此需要设置,不然没有连续的效果
        return (
          <div key={idx} className="scroll-number-list-item">
            <div
              style={{
                transform: `translateY(${currentNthAnimationOffset * 100}%)`
              }}
              className="scroll-number-list-digit-lower"
            >
              {(showNum - 1 + 10) % 10}
            </div>
            <div
              style={{
                transform: `translateY(${currentNthAnimationOffset * 100}%)`
              }}
              className="scroll-number-list-digit-upper"
            >
              {showNum}
            </div>
          </div>
        );
      })}
    </div>
    

使用

const App = () => {
  return <ScrollNumberList />;
};
export default App;

效果

Aug-11-2021 17-17-23.gif