进度条组件(条形)

上一小结中把当前时间和总时间部分做出来了。中间留下了进度条的位置。这章就来实现进度条组件

<template>
  <div class="progress-bar">
    <div class="bar-inner">
      <div class="progress"></div>
      <div class="progress-btn-wrapper">
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
</template>
<style scoped lang="stylus" rel="stylesheet/stylus">
  @import "~common/stylus/variable"

  .progress-bar {
    height 30px
    .bar-inner {
      position relative
      background rgba(0, 0, 0, 0.3)
      top 13px
      height 4px
      .progress {
        position absolute
        background $color-theme
        width 50px
        height 100%
      }
      .progress-btn-wrapper {
        position absolute
        top -13px 
        left -13px
        width 30px
        height 30px
        .progress-btn {
          position: relative
          box-sizing border-box
          width 16px
          height 16px
          border solid 3px $color-text
          border-radius 50%
          top 7px
          left 7px
          background $color-theme
        }
      }
    }
  }
</style>

这里让我学到的知识是,多个小节点通过定位组成一个大的样式。而这里的 垂直居中 都是通过 定位解决的。

比如:progress-btn-wrapper 的宽高都是30px,progress-btn的宽高都是16px,要让progress-btn在容器中垂直水平居中,居然是距离上7px,距离左7px,但是这个 7px 是怎么得来的呢?

通过一翻查看:我们把这两个元素的圆角属性都去掉,都加上背景颜色,就一目了然了。

  1. progress-btn-wrapper : 正方形 宽高30px
  2. progress-btn : 正方形宽高16px
  3. 让progress-btn在容器中居中, progress-btn 自己占用16px,剩下14px,所以上下是7px

1. 进度条移动原理

进度条由以下几个部分组成

  1. bar-inner 进度条背景
  2. progress 进度条
  3. progress-btn 进度条上的控制按钮

移动的原理:

  1. 获取进度条背景宽度
  2. 外部传递要改变的进度条的百分比(外部使用当前播放时间/歌曲总时间计算而来)
  3. 百分比 * 背景进度条的宽度 = 进度的宽度
<template>
  <div class="progress-bar" ref="progressBar">
    <div class="bar-inner">
      <div class="progress" ref="progress"></div>
      <div class="progress-btn-wrapper">
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
</template>
 watch: {
      percent (newPercent) {
        // 1. 获取背景进度条的宽度
        // 2. 百分比 * 背景进度条的宽度就是进度条现在的宽度
        // 3. 设置进度条的宽度
        const progressBarWidth = this.$refs.progressBar.clientWidth
        const progressWidth = newPercent * progressBarWidth
        this.$refs.progress.style.width = `${progressWidth}px`
      }
    }

进度条是实现了,但是有一点不太完美,我们这个进度条由两个小组件组成:

  1. 进度(本来和背景一样宽)
  2. 控制按钮,是圆形,所以进度条实际宽度是 进度条 + 圆形半径

所以 progressBarWidth = 进度条背景宽度 - 按钮宽度,才能算出来符合条件的百分比,这个时候也需要让按钮一起移动才能在视觉上感觉进度条的两个部分是一个部分在移动。

    watch: {
      percent (newPercent) {
        const progressBarWidth = this.$refs.progressBar.clientWidth - PROGRESS_BTN_WIDTH
        const progressWidth = newPercent * progressBarWidth
        this.$refs.progress.style.width = `${progressWidth}px`

        // 圆形按钮移动
        this.$refs.progressBtnWrapper.style.transform = `translateX(${progressWidth}px)`
      }
    }

很奇怪的是,歌曲结束的时候,我这里进度条并没有走完,问题出在 progressBarWidth 上了,应该减去 按钮的宽度,那么按钮在移动的时候 应该是移动多加上自己的半径,但是这里加上半径就会多一点点,半径-1,就刚好,然而进度条在移动的时候,他们两个不能同时移动,有一点细微的缝隙。 不知道为啥视频中的不会有这种问题。

综合以上所述:我认为 应该剪掉 PROGRESS_BTN_WIDTH / 2 的宽度,原因有以下两条

  1. 按钮是整个重合在进度条上的
  2. 按钮的x轴移动 应该是从中心点开始偏移,所以是有 一半的重合的,在移动的时候,刚好另一半会把没有走完的进度条补充完整

2. 进度条 按钮拖动原理

在移动端拖动,是靠几个事件一起合作完成的

  1. touchstart : 可以获得起始点的x,y位置
  2. touchmove : 移动过程
  3. touchend : 可以获得移动完成之后的x,y位置

事件钩子有了,下面来看看怎么利用这个事件钩子找到我们需要的东西

  1. 在touchstart 中,记录起始点位置,记录当前进度条的宽度
  2. 在touchmove中,实时的计算出当前滑动的距离,加上起始点进度条的宽度,得到新的进度条宽度,然后让进度条改变到拖动的位置
  3. 在touchend中,标记当次滑动结束。

3. 进度条 拖动 实现

新增3个移动相关的事件

<template>
  <div class="progress-bar" ref="progressBar">
    <div class="bar-inner">
      <div class="progress" ref="progress"></div>
      <div class="progress-btn-wrapper" ref="progressBtnWrapper"
           @touchstart="onTouchStart"
           @touchmove="onTouchMove"
           @touchend="onTouchEnd"
      >
        <div class="progress-btn"></div>
      </div>
    </div>
  </div>
</template>

下面最终成型的过程如下:

  1. 先给 dom 添加3个移动相关的事件
  2. 在事件中调试,实时获取到 拖动过程中 进度条的宽度,让进度条跟随 拖动而变化
  3. 在拖动事件结束后,计算出当前进度条的百分比,反馈到使用组件的地方(因为外面只关心百分比,我们把进度条相关复杂的操作全部封装成组件了)

关于这三个移动事件,会和浏览器的滑动事件冲突,使用 @touchstart.prevent 解决,但是我在ios10上测试貌似没有发现有什么问题,可能在其他使用场景下会出现问题

<script type="text/ecmascript-6">

  const PROGRESS_BTN_WIDTH = 16

  export default {
    props: {
      /** 进度条百分比 */
      percent: {
        type: Number,
        default: 0
      }
    },
    created() {
      // 先创建属性,否则 在后面的操作中很有可能不确定先后顺序,造成未定义空指针异常
      this.touch = {
        isMoving: false,
        startX: 0,
        left: 0
      }
    },
    data() {
      return {}
    },
    watch: {
      percent(newPercent) {
        // 1. 获取背景进度条的宽度
        // 2. 百分比 * 背景进度条的宽度就是进度条现在的宽度
        // 3. 设置进度条的宽度
        // 4. 由于增加了 滚动条 拖动的功能,这里要判断,在滚动的时候,对外部的百分比变化拒绝接受处理

        if (newPercent < 0 || this.touch.isMoving) {
          return
        }
        const progressBarWidth = this._getProgressBarWidth()
        const progressWidth = newPercent * progressBarWidth
        this._setProgressWidth(progressWidth)
      }
    },
    methods: {
      _getProgressBarWidth() {
        // 进度条该滚动的宽度:进度条 + 圆形按钮 在视觉上刚好等于 背景进度条的宽度,但是圆形按钮移动的位置要减去半径(中心点移动)
        return this.$refs.progressBar.clientWidth - PROGRESS_BTN_WIDTH / 2
      },
      _setProgressWidth(progressWidth) {
        this.$refs.progress.style.width = `${progressWidth}px`
        // 圆形按钮移动
        this.$refs.progressBtnWrapper.style.transform = `translateX(${progressWidth}px)`
      },
      onTouchStart(e) {
        this.touch.isMoving = true // 标识在移动中
        this.touch.startX = e.touches[0].pageX // 记录移动的起始点
        this.touch.left = this.$refs.progress.clientWidth // 记录移动开始时的进度条当前的宽度
      },
      onTouchMove(e) {
        if (!this.touch.isMoving) {
          return
        }

        let deltaX = e.touches[0].pageX - this.touch.startX  // 移动的距离
        // 由于我们的进度条是有固定宽度的,而移动的宽度很有可能超过了这个宽度,所以需要处理边界值
        let progressWidth = this.touch.left + deltaX
        progressWidth = Math.max(0, progressWidth) // 有可能为负数,所以最小为0
        progressWidth = Math.min(this._getProgressBarWidth(), progressWidth) // 有可能超过了 进度条的宽度,所以最大为进度条的宽度
        this._setProgressWidth(progressWidth)
      },
      onTouchEnd() {
        this.touch.isMoving = false // 标识移动结束
        // 并告诉外部当前滚动条新的百分比位置
        this._triggerPercent()
      },
      _triggerPercent() {
        let percent = this.$refs.progress.clientWidth / this._getProgressBarWidth()
        this.$emit('percentChange', percent)
      }
    }
  }
</script>

4. 外部根据进度条的百分比 相关操作

监听到进度条的百分比值,然后根据歌曲的总时间,算出当前歌曲应该从百分比值对应的时间开始播放

 // 进度条被拖动改变的时候
      onPercentChange(percent) {
        // 控制audio的播放器歌曲时间
        let currentTime = this.currentSong.duration * percent
        console.log(currentTime, ' total:', this.currentSong.duration)
        this.$refs.audio.currentTime = currentTime
        if (!this.playing) {  // 如果是暂停状态,在拖动后直接开始播放
          this.togglePlaying()
        }

        // 在这里还有一个现象,待后面的功能解决:拖动到最后,则发现歌曲不播放了,在其他的播放器中拖动进度条在最后,就应该是切下一首歌曲了
      }

5. 点击进度条派发事件

进度条上除了可以拖动外,还可以点击,并且移动到点击的地方

<div class="progress-bar" ref="progressBar" @click="progressClick"> 上添加点击事件。


      progressClick(e) {
        // offsetX/y 能反应在当前dom上点击的左边,x轴,所以x轴的值就是 要移动到的宽度
        let progressWidth = Math.min(e.offsetX, this._getProgressBarWidth())
        this._setProgressWidth(progressWidth)
        console.log(progressWidth)
        this._triggerPercent()
      }

这里我测试出一个bug:在同一个地方点击两次,第一次是正常效果,第二次得到的offsetX则是一个比较小的数值。

© All Rights Reserved            updated 2017-12-28 02:49:41

results matching ""

    No results matching ""