实现一个逐步递增的数字动画

13,292 阅读5分钟

背景

可视化大屏项目使用最多的组件就是数字组件,展示数据的一个变化,为了提高视觉效果,需要给数字增加一个滚动效果,实现一个数字到另一个数字逐步递增的滚动动画。

先上一个思维导图:

思维导图.png

一、实现类似滚轮的效果,容器固定,数字向上滚动

demo1.gif

先列举所有的可能的值形成一个纵向的列表,然后固定一个容器,匀速更改数字的偏移值。

下面来介绍一下这种方案的实现,元素值从0到9一共十个值,每个数字占纵向列表的10%,所以纵向偏移值依次为为0% —> -90%

实现:

<ul>
  <li>
    <span>0123456789</span>
  </li>
</ul>
ul{
  margin-top: 200px;
}
ul li{
  margin:0 auto;
  width: 20px;
  height: 30px;
  text-align: center;
  border:2px solid rgba(221,221,221,1);
  border-radius:4px;
}
ul li span{
  position: absolute;
  color: #fff;
  top: 30%;
  left: 50%;
  transform: translate(-50%,0);
  transition: transform 500ms ease-in-out;
  writing-mode: vertical-rl;
  text-orientation: upright;
  letter-spacing: 17px;
}
let spanDom = document.querySelector('span')
let start = 0
setInterval(() =>{
  start++
  if(start>9){
    start = 0
  }
  spanDom.style.transform = `translate(-50%,-${start*10}%)`
}, 1000)

上述代码存在一个问题,当我们从9到0的时候,容器偏移从-90%直接到了0%。 但是由于设定了固定的过渡动画时间,就会出现一个向反方向滚动的情况,为了解决这个问题,可以参考无缝滚动的思路

  • 在9后面复制一份0,

  • 当纵向列表滚动到9的时候,继续滚动到复制的0

  • 滚动到复制的0的时候,把列表的偏移位置改为0,并且控制动画时间为0

<ul>
  <li>
    <span>01234567890</span>
  </li>
</ul>
let spanDom = document.querySelector('span')
let start = 0
var timer = setInterval(fn, 1000);
function fn() {
  start++
  clearInterval(timer)
  timer = setInterval(fn,start >10 ? 0 : 1000);
  if(start>10){
    spanDom.style.transition = `none`
    start = 0
  }else{
    spanDom.style.transition = `transform 500ms ease-in-out`
  }
  spanDom.style.transform = `translate(-50%,-${start/11*100}%)`
}

demo2.gif

利用两个元素实现滚动

仔细看动图的效果,事实上在在视口只有两个元素,一个值之前的值,一个为当前的值,滚动偏移值只需设置translateY(-100%)

具体思路:

  • 声明两个变量,分别存放之前的值prev,以及变化后的值cur;声明一个变量play作为这两个值的滚动动画的开关

  • 使用useEffect监听监听传入的值:如果是有效的数字,那么把没有变化前的值赋值给prev,把当前传入的值赋值给cur,并且设置palytrue开启滚动动画

下面是调整后的代码结构:

 <div className={styles.slider}>
   {[prev, cur].map((item, index) => (
      <span key={index} className={`${styles['slider-text']} ${playing && styles['slider-ani']} ${(prev === 0 && cur === 0 && index ===0) && styles['slider-hide']}`}>
        {item}
      </span>
    ))}
  </div>
const { value} = props
const [prev, setPrev] = useState(0)
const [cur, setCur] = useState(0)
const [playing, setPlaying] = useState(false)

  const play = (pre, current) => {
    setPrev(pre)
    setCur(current)
    setPlaying(false)
    setTimeout(() => {
      setPlaying(true)
    }, 20)
  }

  useEffect(() => {
    if (!Number.isNaN(value)) {
      play(cur, value)
    } else {
      setPrev(value)
      setCur(value)
    }
  }, [value])
.slider {
  display: flex;
  flex-direction: column;
  height: 36px;
  margin-top: 24%;
  overflow: hidden;
  text-align: left;
}

.slider-text {
  display: block;
  height: 100%;
  transform: translateY(0%);
}

.slider-ani {
  transform: translateY(-100%);
  transition: transform 1s ease;
}
.slider-hide {
  opacity: 0;
}

实现多个滚轮的向上滚动的数字组件

组件.gif

利用H5的requestAnimationFrame()API实现数字逐步递增的动画效果

实现一个数字的逐渐递增的滚动动画,并且要在指定时间内完成。要看到流畅的动画效果,就需要在更新元素状态时以一定的频率进行,JS动画都是通过在很短的时间内不停的渲染/绘制元素做到的,所以计时器一直都是Javascript动画的核心技术,关键就是刷新的间隔时间,刷新时间需要尽量短,这样动画效果才能显得更加流畅,不卡顿;同时刷新间隔又不能太短,需要确保浏览器有能力渲染动画

大多数电脑显示器的刷新频率是 60Hz,即每秒重绘 60次。因此平滑动画的最佳循环间隔是通常是 1000ms/60,约等于16.6ms

计时器对比

  • 与 setTimeout 和 setInterval 不同,requestAnimationFrame 不需要程序员自己设置时间间隔。setTimeout 和 setInterval 的问题是精确度低。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。

  • requestAnimationFrame 采用系统时间间隔,它会要求浏览器根据自己的频率进行一次重绘,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。

  • requestAnimationFrame 对于隐藏或不可见元素,将不会进行重绘或回流,就意味着使用更少的 CPU、GPU 和内存使用量。

  • requestAnimationFrame 是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销。

requestAnimationFrame实现滚动动画思路

  • 动画开始,记录开始动画的时间 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]);

  • 针对当前每个数字位上的数字进行比较,如果有变化,进行偏移量的变化,偏移量体现在当前数字位上的数字与下一位数字之间的差值,这个变化每一帧都串起来形成了滚动动画

成果展示

成果.gif

成果2.gif