列表无缝滚动

782 阅读4分钟

我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!

code.juejin.cn/pen/7146141…

起源

是因为看到,实时信息的滚动播放。我就在思考,这是怎么实现的呢?

思路分析

  1. HTML结构,有一个存放列表的容器。
  2. 滚动。支持上下左右的滚动。
  3. 步长。
  4. hover停止。
  5. 无缝滚动。

实现

这里使用了vue-seamless-scroll库。快速实现列表无缝滚动。

源码解读

  1. requestAnimationFrame 实现动画的API

requestAnimationFrame是浏览器提供用来绘制动画一个api。它会在浏览器的每一帧Layout,Paint之前执行,就是常说的回流和重绘之前执行,这样可以很方便的执行一段更改样式的代码,然后浏览器就能马上回流,重绘,生成一帧。 Window.requestAnimationFrame()

优势:

CPU节能:

使用SetTinterval 实现的动画,当页面被隐藏或最小化时,SetTinterval 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。

而RequestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统走的RequestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

函数节流:

在高频率事件( resize, scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,RequestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销,一个刷新间隔内函数执行多次时没有意义的,因为多数显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

减少DOM操作:

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

setTimeout执行动画的缺点:

它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是: settimeout任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;

settimeout的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。

参考文章

  1. 滚动方向 滚动的方向是通过css样式来控制。

    pos () {
      return {
        transform: `translate(${this.xPos}px,${this.yPos}px)`,
        transition: `all ${this.ease} ${this.delay}ms`,
        overflow: 'hidden'
      }
    },
    
  2. 无缝连接

    1. 通过copyHtml的方式,复制一个相同的节点模块,接在dom节点之后。这样每次循环的时候,就是无缝的了。这里的思想和图片无缝轮播是一样的。无缝轮播的实现思路
           <div
          ref="realBox"
          :style="pos"
          @mouseenter="enter"
          @mouseleave="leave"
          @touchstart="touchStart"
          @touchmove="touchMove"
          @touchend="touchEnd"
        >
          <div ref="slotList" :style="float">
            <slot></slot>
          </div>
          <div v-html="copyHtml" :style="float"></div>
        </div>
    
            _initMove () {
            this.$nextTick(() => {
              const { switchDelay } = this.options
              const { autoPlay, isHorizontal } = this
              this._dataWarm(this.data)
              this.copyHtml = '' //清空copy
              if (isHorizontal) {
                this.height = this.$refs.wrap.offsetHeight
                this.width = this.$refs.wrap.offsetWidth
                let slotListWidth = this.$refs.slotList.offsetWidth
                // 水平滚动设置warp width
                if (autoPlay) {
                  // 修正offsetWidth四舍五入
                  slotListWidth = slotListWidth * 2 + 1
                }
                this.$refs.realBox.style.width = slotListWidth + 'px'
                this.realBoxWidth = slotListWidth
              }
    
              if (autoPlay) {
                this.ease = 'ease-in'
                this.delay = 0
              } else {
                this.ease = 'linear'
                this.delay = switchDelay
                return
              }
    
              // 是否可以滚动判断
              if (this.scrollSwitch) {
                let timer
                if (timer) clearTimeout(timer)
                this.copyHtml = this.$refs.slotList.innerHTML
                setTimeout(() => {
                  this.realBoxHeight = this.$refs.realBox.offsetHeight
                  this._move()
                }, 0);
              } else {
                this._cancle()
                this.yPos = this.xPos = 0
              }
            })
          },
    
  3. 运行起来

    mounted () {
    	this._initMove()// 在调用 _move()
    },
    
          _move () {
            // 鼠标移入时拦截_move()
            if (this.isHover) return
            this._cancle() //进入move立即先清除动画 防止频繁touchMove导致多动画同时进行
            this.reqFrame = requestAnimationFrame(
              function () {
                const h = this.realBoxHeight / 2  //实际高度
                const w = this.realBoxWidth / 2 //宽度
                let { direction, waitTime } = this.options
                let { step } = this
                if (direction === 1) { // 上
                  if (Math.abs(this.yPos) >= h) { // 重点: 这里基于高度做判断处理,如果第一个div,向上移动完成之后,条件成立,y值重置为0;
                    this.$emit('ScrollEnd')
                    this.yPos = 0
                  }
                  this.yPos -= step
                } else if (direction === 0) { // 下
                  if (this.yPos >= 0) {
                    this.$emit('ScrollEnd')
                    this.yPos = h * -1
                  }
                  this.yPos += step
                } else if (direction === 2) { // 左
                  if (Math.abs(this.xPos) >= w) {
                    this.$emit('ScrollEnd')
                    this.xPos = 0
                  }
                  this.xPos -= step
                } else if (direction === 3) { // 右
                  if (this.xPos >= 0) {
                    this.$emit('ScrollEnd')
                    this.xPos = w * -1
                  }
                  this.xPos += step
                }
                if (this.singleWaitTime) clearTimeout(this.singleWaitTime)
                if (!!this.realSingleStopHeight) { //是否启动了单行暂停配置
                  if (Math.abs(this.yPos) % this.realSingleStopHeight < step) { // 符合条件暂停waitTime
                    this.singleWaitTime = setTimeout(() => {
                      this._move()
                    }, waitTime)
                  } else {
                    this._move()
                  }
                } else if (!!this.realSingleStopWidth) {
                  if (Math.abs(this.xPos) % this.realSingleStopWidth < step) { // 符合条件暂停waitTime
                    this.singleWaitTime = setTimeout(() => {
                      this._move()
                    }, waitTime)
                  } else {
                    this._move()
                  }
                } else {
                  this._move()
                }
              }.bind(this)
            )
          },
    

运行起来

  1. mounted中调用this._initMove()。_initMove中,进行了节点的复制,解决无缝连接;计算容器的高度记录。
  2. 在调用 _move()。进行高度的判断,进行x或者y值得计算,设置transform,translate,x,y值得修改;transition的动画设置;requestAnimationFrame()进行调动;进行循环调用this._move();
  3. 通过计算属性,更新css样式。:style="pos"

vue-seamless-scroll

使用指南

注意项

1.最外层容器需要手动设置width、height、overflow:hidden

2.左右的无缝滚动需要给主内容区域(即默认slot插槽提供)设定合适的css width属性(否则无法正确计算实际宽度)。 也可以通过给他设置为display:flex;无需设置css width属性

3.step值不建议太小,不然会有卡顿效果(如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~,比如单步设置的30,step不能为4)

4.需要实现手动切换左右滚动的时候,必须设置autoPlay:false(1.1.17版本开始,只需要设置navigation:false),目前不支持环路

5.提供了slot left-switch || right-switch可以自由定义需要的按钮样式 外层有div已经定位了位置居中,距离两边侧的距离可以通过switchOffset参数调整

6.当按钮到达边界位置,会自动为无法点击状态按钮加上定义的switchDisabledClass: 'disabled',可以按需配置