实现一个基于`motion/react`的数字滚动组件

230 阅读1分钟

本文基于motion/react的开源API开发了一个简单的数字滚动组件。

免责声明: 该组件与motion/reactAnimateNumber无关,本文未参考上述组件的实现,不是上述组件的替代。

码上尝鲜

教程

还没写...

完整实现

'use client'
import clsx from 'clsx'
import { animate, AnimationPlaybackControlsWithThen, motion, Transition } from 'motion/react'
import { HTMLAttributes, ReactNode, useEffect, useRef, useState } from 'react'

export interface NumberAnimationProps extends NumberScrollProps {
  value?: number
  format?: (value: number) => number | string
  initial?: number
  delay?: number
}

export default function NumberAnimation({
  value: newValue = 0,
  format,
  initial,
  delay,
  ...rest
}: NumberAnimationProps) {
  const [value, setValue] = useState(initial ?? 0)
  const animateRef = useRef<AnimationPlaybackControlsWithThen>(null)

  useEffect(() => {
    if (animateRef.current) {
      animateRef.current.stop()
    }
    if (delay && value !== newValue) {
      animateRef.current = animate(
        { number: value },
        { number: newValue },
        {
          onUpdate(last) {
            setValue(last as unknown as number)
          },
          onComplete() {
            animateRef.current = null
          },
          duration: delay,
        }
      )
    } else {
      setValue(newValue)
    }
    return () => {
      if (animateRef.current) {
        animateRef.current.stop()
      }
    }
  }, [newValue])

  return (
    <NumberScroll {...rest} value={delay ? (format ? format(value) : Math.round(value)) : value} />
  )
}

export interface NumberScrollProps extends HTMLAttributes<HTMLSpanElement> {
  value?: number | string
  className?: string
  transition?: Transition
}

export function NumberScroll({ value = 0, className, transition, ...rest }: NumberScrollProps) {
  const v = String(value).split('')

  return (
    <span
      {...rest}
      className={clsx(
        'inline-block overflow-hidden h-[1.5em] px-[.2em] box-content mask-number-scroll',
        className
      )}
    >
      {v.map((e, i) => (
        <SingleNumber key={i} value={e} transition={transition} />
      ))}
    </span>
  )
}

export interface SingleNumberProps {
  value: string
  transition?: Transition
}

function SingleNumber({ value, transition }: SingleNumberProps) {
  const nodes: ReactNode[] = []
  const v = +value

  for (let i = 0; i < 10; i++) {
    nodes.push(
      <span className="h-[1.5em] leading-[1.5em] shrink-0" key={i}>
        {i}
      </span>
    )
  }

  if (Number.isNaN(v)) {
    return <span className="inline-flex flex-col h-[1.5em]">{value}</span>
  }

  return (
    <motion.span
      transition={
        transition ?? {
          type: 'tween',
          ease: 'easeOut',
          duration: .3,
        }
      }
      animate={{
        y: `-${v}00%`,
      }}
      className="inline-flex items-center flex-col h-[1.5em]"
    >
      {nodes}
    </motion.span>
  )
}

在线预览

  • Codesandbox / 第一次打开会很慢,耐心等待或者看其他链接
  • 码上掘金 / 适合查看效果,不适合看源码
  • Github gist / 只能看源码不能运行