Vue | 原生audio样式不好看,自己写一个简易的音乐播放控件,实现播放暂停可拖动

最近遇到一个需求,需要写一个简单的音频控件:往控件传入音频url实现播放、暂停、允许拖动、显示时间等功能,由于原生audio标签样式不太好看,所以决定自己写一个。效果如下:
在这里插入图片描述

思路

把这个需求分成几个部分:左边的播放暂停按钮、右边上面的文字显示、下面的进度条。

可以把audio作为原始控件(但不在页面中显示),用自己写的控件来控制audio标签的行为动作。

1.暂停、播放按钮控制audio的play()pause()

2.进度条,这里可以用range来实现,修改range的样式来达到上图的效果,通过onInputonChange来监控range拖动的变化,进而控制audio的变化(播放到指定位置)。

3.文件名字样以及右边的时间显示可以通过audio标签的durationcurrentTime属性来控制。

实现

vue项目中使用了fontawesome图表库,可以根据实际自己替换。

html

html结构分为左边的图标跟右边的文字跟range控件,基本结构也比较简单。

html中必须有一个audio标签,将这个标签隐藏即可,然后用过自己写的控件来控制audio

<audio style="display: none" controls></audio>

CSS

这里主要是对range控件的样式设置,通过input[type="range"]控制进度条的样式,通过input[type='range']::-webkit-slider-thumb控制滑块的样式。

input[type='range'] {
    outline: none;
    -webkit-appearance: none; /*清除系统默认样式*/
    width: 100% !important;
    background: -webkit-linear-gradient(#10a9ff, #10a9ff) no-repeat, #dddddd; /*背景颜色,俩个颜色分别对应上下*/
    background-size: 0% 100%; /*设置左右宽度比例,这里可以设置为拖动条属性*/
    height: 2px; /*横条的高度,细的真的比较好看嗯*/
}
/*拖动块的样式*/
input[type='range']::-webkit-slider-thumb {
    -webkit-appearance: none; /*清除系统默认样式*/
    height: 10px; /*拖动块高度*/
    width: 3px; /*拖动块宽度*/
    background: #10a9ff; /*拖动块背景*/
}

逻辑

大体思路是:由两个icon和range控制audio,由audio控制文字的显示。

1.播放暂停图标控制audio标签播放。

audio元素有pause()play()控制音频的暂停和播放。因此,可以对icon绑定事件控制audio的播放暂停。

<span class="icon" v-if="isPlay == false" @click="play">
    <i class="fa fa-play play-icon" aria-hidden="true"></i>
</span>
<span class="icon" v-if="isPlay == true" @click="pause">
    <i class="fa fa-pause pause-icon" aria-hidden="true"></i>
</span>
// 控制音乐播放
play() {
    const audio = this.$refs.audio;
    audio.play();
    this.isPlay = true;
},
// 控制音乐暂停
pause() {
    const audio = this.$refs.audio;
    audio.pause();
    this.isPlay = false;
}

isPlay用来控制显示哪一个icon。

2.rang控制audio元素的时间进度(range拖动的时候)。

rang实际上是一个input标签,因此有onChangeonInput两个回调事件监控range相关参数的变化。audio元素有一个currentTime属性可以设置audio标签进度(即到达指定时间),可以通过监控range的变化,实时更改audiocurrentTime来实现拖动改变音频进度。

<input type="range" ref="range" @input="onChange" @change="onChange" min="0" max="360" value="0">

注:这里将range分成了360份,后面的计算均和360相关,当然可以根据自己的需要设置。

// range--拖动进度条得到的回调
onChange() {
    let value = this.$refs.range.value;
   	// 控制进度条样式
    const persentage = ((value / 360) * 100).toFixed(1) + '%';
    this.$refs.range.style.backgroundSize = `${persentage} 100%`; // 拖动的时候,需要跟着改变进度条样式
    // 控制音频播放
    const timeToGo = (value / 360) * this.totalTime;
    const audio = this.$refs.audio;
    audio.currentTime = timeToGo;
}
3.audio播放的时候,控制range也跟着动。

这个发生在range控件不被人为拖动时,需要跟着audio变化(包括样式和滑块的位置)

audio标签有方法ontimeupdate来监控audio变化,每次audio的进度变化的时候,就会执行回调。因此,可以通过绑定一个事件来实现range跟着audio变化。其中,range滑块的位置可以通过设置range的属性值value来改变,value的值在minmax之间。

<audio style="display: none" :src="audioURL" ref="audio" controls @timeupdate="update"></audio>
// audio--进度变化的时候的回调--改变文字
update() {
    const audio = this.$refs.audio;
    const currentTime = audio.currentTime; // 当前播放时间
    this.currentTime = currentTime;
    // 改变进度条的值
    const range = this.$refs.range;
    range.value = ((this.currentTime / this.totalTime) * 360).toFixed(1);
    // 进度条的值改变的时候,颜色也跟着变化
    const persentage = ((this.currentTime / this.totalTime) * 100).toFixed(1) + '%';
    this.$refs.range.style.backgroundSize = `${persentage} 100%`;
}
4.文件名的获取。

由于需求是传入音频url的音频播放控件,因此,可以对传入的url进行分析。

一般URL的格式是https://xxx/xxx/xxx/xxxx.mp3,可以基于/对URL分解得到数组,获取最后一个元素即为文件名。

// 获取文件名称
getFilename(url) {
    const arr = url.split('/');
    return arr[arr.length - 1];
},
5.当前时间与总时间的显示。

audio标签有durationcurrentTime属性来获取当前音频播放进度以及总时间(单位是秒)。获取到当前时间以及播放总时间后,进行格式处理之后就可以绑定到相关的位置上。

<div class="words flex-between">
    <div class="name">{{  audioName==null ? "未知": audioName}}</div>
    <div class="time">{{ formatCurrentTime }} / {{ formatTotalTime }}</div>
</div>

formatCurrentTimeformatTotalTime是由currentTimetotalTime通过计算属性返回的,下面只分析currentTimetotalTime的获取。

// audio--进度变化的时候的回调--改变文字
update() {
    const audio = this.$refs.audio
    const currentTime = audio.currentTime // 当前播放时间
    this.currentTime = currentTime
},

总时间在音频加载完毕后就进行记录,通过绑定audioonCanplay回调来实现。

<audio style="display: none" :src="audioURL" ref="audio" controls @timeupdate="update" @canplay="loadingFinish"></audio>
loadingFinish() {
    const totalTime = this.$refs.audio.duration
    this.totalTime = totalTime
}
6.prop设置。

传入一个音频URL,用于显示文件名以及作为audio的音源链接。

最后

当然,样式可以更"花里胡哨" 。主要是了解了audio标签的onUpdatetime()回调、onCanplay()回调;rangevalue属性、onChange()回调、onInput()回调。功能也可以进一步扩展,后面再添加吧~

完整代码

<template>
  <div class="container">
    <div class="audio-container">
      <div class="left">
        <span class="icon" v-if="isPlay == false" @click="play">
          <i class="fa fa-play play-icon" aria-hidden="true"></i>
        </span>
        <span class="icon" v-if="isPlay == true" @click="pause">
          <i class="fa fa-pause pause-icon" aria-hidden="true"></i>
        </span>
      </div>
      <div class="right">
        <div class="words flex-between">
          <div class="name">{{  audioName==null ? "未知": audioName}}</div>
          <div class="time">{{ formatCurrentTime }} / {{ formatTotalTime }}</div>
        </div>
        <div class="duration">
          <input type="range" ref="range" @input="onChange" @change="onChange" min="0" max="360" value="0">
        </div>
      </div>
    </div>
    <audio style="display: none" :src="audioURL" ref="audio" controls @timeupdate="update" @canplay="loadingFinish"></audio>
  </div>
</template>

<script>
export default {
  name: 'AudioPlayer',
  components: {},
  props: {
    audioURL: {
      type: String,
      default: '../未知',
    },
  },
  data() {
    return {
      isPlay: false, // 控制icon切换
      totalTime: 0, // 播放总时间--秒
      currentTime: 0, // 当前播放时间--秒
    }
  },
  computed: {
    formatTotalTime() {
      return this.formatTime(this.totalTime)
    },
    formatCurrentTime() {
      return this.formatTime(this.currentTime)
    },
    // 音频名称
    audioName() {
      return this.getFilename(this.audioURL)
    },
  },
  mounted() {
    this.$refs.audio.src = this.audioURL
    // 将range位置归0--如果不使用这个的话,设置了value之后没有用= =郁闷
    setTimeout(() => {
      this.$refs.range.value = 0
    }, 1)
  },
  methods: {
    // 控制音乐播放
    play() {
      const audio = this.$refs.audio
      audio.play()
      this.isPlay = true
    },
    // 控制音乐暂停
    pause() {
      const audio = this.$refs.audio
      audio.pause()
      this.isPlay = false
    },
    // 音乐缓存完毕,获取时间
    loadingFinish() {
      const totalTime = this.$refs.audio.duration
      this.totalTime = totalTime
    },
    // range--拖动进度条得到的回调
    onChange() {
      let value = this.$refs.range.value
      const persentage = ((value / 360) * 100).toFixed(1) + '%'
      this.$refs.range.style.backgroundSize = `${persentage} 100%`
      // 控制音频播放
      const timeToGo = (value / 360) * this.totalTime
      const audio = this.$refs.audio
      audio.currentTime = timeToGo
    },
    // audio--进度变化的时候的回调--改变文字
    update() {
      const audio = this.$refs.audio
      const currentTime = audio.currentTime // 当前播放时间
      this.currentTime = currentTime
      // 改变进度条的值
      const range = this.$refs.range
      range.value = ((this.currentTime / this.totalTime) * 360).toFixed(1)
      // 进度条的值改变的时候,颜色也跟着变化
      const persentage = ((this.currentTime / this.totalTime) * 100).toFixed(1) + '%'
      this.$refs.range.style.backgroundSize = `${persentage} 100%`
    },

    //辅助函数,将秒变成分秒的形式--用在计算属性中
    formatTime(value) {
      let second = 0
      let minute = 0
      minute = parseInt(value / 60)
      second = parseInt(value % 60)
      // 补0
      minute = minute < 10 ? '0' + minute : minute
      second = second < 10 ? '0' + second : second
      return minute + ':' + second
    },
    // 通过url获取filename
    getFilename(url) {
      const arr = url.split('/')
      return arr[arr.length - 1]
    },
  },
}
</script>

<style scoped>
.audio-container {
  padding: 8px 16px;
  width: 100%;
  background: #f5f6f8;
  border-radius: 2px;
  display: flex;
}
.left {
  margin-right: 16px;
}
// 播放暂停按钮
.icon {
  display: inline-block;
  width: 28px;
  height: 28px;
  border: 2px solid #10a9ff;
  border-radius: 50%;

  text-align: center;
  font-size: 16px;
  line-height: 28px;

  color: $base-color-blue;
}
.icon:hover {
  cursor: pointer;
}
.play-icon {
  position: relative;
  left: 2px;
}
.flex-between {
  display: flex;
  justify-content: space-between;
  align-content: center;
}
.right {
  flex: 1;
}
.words {
  margin-bottom: -1px;
}
.name {
  font-size: 14px;
  color: #333333;
  line-height: 14px;
}
.time {
  font-size: 14px;
  color: #666666;
  line-height: 14px;
}
// 控件
input[type='range'] {
  outline: none;
  -webkit-appearance: none; /*清除系统默认样式*/
  width: 100% !important;
  background: -webkit-linear-gradient(#10a9ff, #10a9ff) no-repeat, #dddddd; /*背景颜色,俩个颜色分别对应上下*/
  background-size: 0% 100%; /*设置左右宽度比例,这里可以设置为拖动条属性*/
  height: 2px; /*横条的高度,细的真的比较好看嗯*/
}
/*拖动块的样式*/
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none; /*清除系统默认样式*/
  height: 10px; /*拖动块高度*/
  width: 3px; /*拖动块宽度*/
  background: #10a9ff; /*拖动块背景*/
}
</style>
  • 7
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值