7

NUTUI-React 数字滚动组件的设计与实现

 1 year ago
source link: https://jelly.jd.com/article/63aee183abf18f0057c794ec
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
NUTUI-React 数字滚动组件的设计与实现
上传日期:2022.12.31
NutUI-React 是一套京东风格的轻量级移动端 React 组件库,覆盖移动端主流业务场景。React 版本从今年1月发版以来,陆续发布了 85 个大小版本,截止目前已完成 60+ 组件数量的建设。 本文主要介绍基于 React 的 数字滚动组件的设计与实现原理,下面一起看看组件的是如何实现的。

CountUp 组件是 NutUI-React 的一个数字滚动组件,用于实现数字类似老虎机一样的滚动效果,在项目中,我们经常会遇到数字滚动的需求,例如省钱金额的突出展示。我们通过一张效果图,来看看 CountUp 组件大概实现什么功能。

4127f0d0dd6c3fdd.gif

本文会通过以下两方面,来介绍 CountUp 组件的具体实现:

  • 为什么要封装该组件
  • 组件的实现思路和实现原理

为什么要封装该组件

很多业务场景下,我们需要突出展示一串数字,这串数字可以是写死固定在页面上的,也可以是动态刷新实时请求的,数字中间可能有千位分隔符或是小数点,我们想做类似老虎机的滚动效果。当业务达到一定规模后,会遇到很多相似功能,每次重新开发,不仅会影响开发效律,且这些相近的代码可能潜伏某些问题,一旦暴露,需要花费大量去处理业务里的相同代码。如果我们可以把相同的代码进行抽离封装,开发效律和维护会得到质的飞跃。

组件的实现思路和实现原理

实现思路:将传入的 N 位数字拆分成每一个要滚动的目标数字,然后创建每一组滚动数字的容器,容器内创建从 0 - 9 的 元素,纵向排列,就像一个竖着写着从 0 - 9 的长纸条,然后让它在指定时间内从 0 上拉到目标数字。

有了思路,我们开始着手组件实现,我们需要设定一些可变的 Props 方便后期不同场景使用,分别为 endNumber 要展示的结束值、maxLen 最大展示长度(长度不够按位补 0)、delaySpeed 等待动画执行时间、easeSpeed 动画执行时间、 thousands 是否有千位分隔符。定义好 Props 之后,基于实现思路,把功能进行细化,开始我们具体的实现。

首先,把 endNumber 值进行拆分处理,把每一个要滚动的目标数字提取出来依次存入到数组,如果支持千位分隔符或是小数点,需要把它们插入到数组指定的位置, 如果设置了展示长度,但数组的长度不够,需要在数组前面插入 0 进行补位。

const getShowNumber = () => {
    const splitArr = endNumber.split('.')
    // 如果小于展示长度,添加 "0" 补位
    const intNumber =
      maxLen && splitArr[0].length < maxLen
        ? (Array(maxLen).join('0') + splitArr[0]).slice(-maxLen)
        : splitArr[0]
    // 如果支持千位分隔符,通过正则把千位分隔符插入到数组指定的位置
    const currNumber = `${
      thousands ? intNumber.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,') : intNumber
    }${splitArr[1] ? '.' : ''}${splitArr[1] || ''}`
    return currNumber.split('')
}

处理完 endNumber 值拆分的数组,我们定义一下 DOM 结构,我们要对每一个目标数字模拟 0 - 9 的长纸条,由于我们会遇到数字是 0 的时候,但我们又想要滚动效果,可以再初始化渲染的时候设置两次 0 - 9 的长纸条堆叠,如图所示

11d36b920f7b79f2.png

具体实现如下

// 根据定义的动画执行时间 easeSpeed,元素滚动时执行 ease-in-out 效果
const numberEaseStyle: CSSProperties = {
    transition: `transform ${easeSpeed}s ease-in-out`,
}
return (
    <div className={`${b()} ${className}`} ref={countupRef}>
        <ul className={b('list')}>
        {numerArr.map((item: string, idx: number) => {
            return (
            <li
                className={`${b('listitem', {
                number: !Number.isNaN(Number(item)),
                })}`}
                key={idx}
            >
                {/* 判断是否是数字,如果是数字,模拟 0 - 9 的长纸条,如果非数字,按符号处理 */}
                {!Number.isNaN(Number(item)) ? (
                <span className={b('number')} style={numberEaseStyle}>
                    {[...numbers, ...numbers].map((number, subidx) => {
                    return <span key={subidx}>{number}</span>
                    })}
                </span>
                ) : (
                <span className={b('separator')}>{item}</span>
                )}
            </li>
            )
        })}
        </ul>
    </div>
)

定义好 DOM 结构之后,我们要为每一个目标数字设置滚动效果,首先拿到每组容器的外层元素,把该元素高度分割成 20 等份,根据当前列所需要渲染的目标数字,结合CSS transform 对每组容器进行纵向滚动,如图所示

11d36b920f7b79f2.png

具体实现如下

  const setNumberTransform = () => {
    if (countupRef.current) {
      const numberItems = countupRef.current.querySelectorAll(
        '.nut-countup__number'
      )
      const numberFilterArr: Array<string> = numerArr.filter(
        (item: string) => !Number.isNaN(Number(item))
      )
      Object.keys(numberItems).forEach((key) => {
        const elem = numberItems[Number(key)] as HTMLElement
        const idx = Number(numberFilterArr[Number(key)])
        if ((idx || idx === 0) && elem) {
          // 父元素和实际列表高度的百分比,分割成20等份
          const transform = `translate(0, -${(idx === 0 ? 10 : idx) * 5}%)`
          elem.style.transform = transform
          elem.style.webkitTransform = transform
        }
      })
    }
  }

定义好 滚动效果之后,我们利用 setTimeout 和定义好的等待动画执行时间 delaySpeed,初始化滚动效果,这样我们的组件就完成了

useEffect(() => {
    timerRef.current = window.setTimeout(() => {
      setNumberTransform()
    }, delaySpeed)
    return () => {
      window.clearTimeout(timerRef.current)
    }
}, [])

本篇文章介绍了 CountUp 组件的实现思路和实现原理,希望对大家有那么一丢丢的帮助或启发。后续我们也会对组件进行持续优化迭代,如果感兴趣,不妨查看和试用一下,使用上有任何问题,可在 issues 上进行提问,我们会尽快解答和修复。如果文章感兴趣,记得点个 Star 支持我们一下~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK