VUE实现模拟播放器
作者:岩雷
项目背景
默认音乐播放器在不同的浏览器有不同的展现形式,且UI过于单一,不能满足页面整体UI一致性,从而不能满足业务的需求。
效果展示
audio基础知识
属性:
autoplay:音频自动播放;
controls:提供一个包含声音,播放进度,播放暂停的控制面板,让用户可以控制音频的播放;
currentTime:当前播放时间;
duration:音频总长度;
loop: 循环播放;
muted: 静音;
src:音频的URL;
方法:
play: 播放;
pause: 暂停;
timeupdate:当currentTime更新时会触发timeupdate事件
实现思路
1、点击按钮实现播放与暂停
2、默认播放实时改变进度条的宽度和滑动图标的位置
3、点击进度条改变音频的进度
4、划动进度条改变音频的进度
PS:进度条点击和滑动进度图标增加点击范围
实现流程
概念名词:
- 默认滑动条
- 进度滑动条
- 滑动进度图标
不添加controls,默认的播放器会隐藏
<audio
src="./aa.mp3"
controls="controls"
loop
class="audio1"
@timeupdate="updateTime"
ref="audio"
></audio>
实现模拟音乐播放器,主要需要监听timeupdate方法,currentTime当前的播放进度实时更新,从而实时改变进度条和滑动图标的位置。
1、点击按钮实现播放与暂停
data中添加是否播放参数isPlay(默认false),通过监听isPlay的变化计算显示播放按钮还是暂停按钮
computed: {
// 播放按钮显示
isPlayOrPause() {
return this.isPlay ? "pause-circle-o" : "play-circle-o";
}
// 播放进度比例,总长度也课通过duration获取
percent() {
return this.currentTime / this.totalTime;
},
}
2、监听timeupdate方法,存储当前的播放进度
updateTime(e) {
this.currentTime = e.target.currentTime;
}
3、默认播放,监听percent变化,改变进度条和滑动图标的位置
watch: {
percent(newPrecent) {
// 非滑动进度图标时间内
if (newPrecent >= 0&& !this.touch.initiated) {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
// 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
const offsetWidth = newPrecent * barWidth
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth)
}
}
}
4、点击默认滑动条,更改音频播放进度
progressClick(e) {
// getBoundingClientRect用于获取某个元素相对于视窗的位置集合
const rect = this.$refs.progressBar.getBoundingClientRect();
// pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
const offsetWidth = e.pageX - rect.left;
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth);
// 计算当前播放时间
this._triggerPercent();
},
// 计算当前时间
_triggerPercent() {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth =
this.$refs.progressBar.clientWidth - progressBtnWidth;
// 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
const percent = this.$refs.progress.clientWidth / barWidth;
// 当前播放时长 = 总时长 * 播放比例
const currentTime = this.totalTime * percent;
// 设置音频当前播放时间
this.$refs.audio.currentTime = currentTime;
},
// 计算拖动条的宽度和滑动偏移的距离
_offset(offsetWidth) {
this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
},
5、滑动进度图标,更改音频播放进度,分三个阶段:
- 滑动开始(start):记录当前点距离页面左侧的距离和进度滑动条的宽度
- 滑动中(move):实时记录进度滑动条的偏移量,从而计算进度滑动条和滑动进度图标的当前位置
- 滑动结束(end):设置音频当前播放时间
progressTouchStart(e) {
this.touch.initiated = true; // 标志位 表示初始化
this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
console.log("start", this.touch.startX);
},
progressTouchMove(e) {
console.log("move");
if (!this.touch.initiated) {
return;
}
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth =
this.$refs.progressBar.clientWidth - progressBtnWidth;
const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
// 滑动中进度滑动条和滑动进度图标的当前位置
const offsetWidth = Math.min(
barWidth,
Math.max(0, this.touch.left + deltaX)
);
this._offset(offsetWidth);
},
progressTouchEnd() {
console.log("end");
this.touch.initiated = false;
this._triggerPercent();
},
最终代码
<template>
<div class="audio-wrapper">
<audio
src="./aa.mp3"
controls="controls"
loop
class="audio1"
@timeupdate="updateTime"
ref="audio"
></audio>
<div class="fh-progress-wrapper">
<span class="fh-time fh-time-l">{{ format(currentTime) }}</span>
<div class="fh-progress-bar-wrapper">
<div
class="fh-progress-bar"
ref="progressBar"
@click="progressClick"
>
<div class="fh-bar-inner">
<div class="fh-progress" ref="progress"></div>
<div
class="fh-progress-btn-wrapper"
ref="progressBtn"
@touchstart.prevent="progressTouchStart"
@touchmove.prevent="progressTouchMove"
@touchend="progressTouchEnd"
>
<div class="fh-progress-btn"></div>
</div>
</div>
</div>
</div>
<span class="fh-time fh-time-r">{{ format(totalTime) }}</span>
</div>
<div class="isPlayOrPause" @click="onHandle">
<van-icon :name="isPlayOrPause" />
</div>
</div>
</template>
<script>
const progressBtnWidth = 16;
import { Icon } from "vant";
export default {
name: "index",
components: {
[Icon.name]: Icon
},
data() {
return {
currentTime: 0, // 当前播放时间
touch: {}, // 滑动图标时记录的参数
isPlay: false, // 是否播放
totalTime: 23
};
},
computed: {
percent() {
return this.currentTime / this.totalTime;
},
isPlayOrPause() {
return this.isPlay ? "pause-circle-o" : "play-circle-o";
}
},
methods: {
onHandle() {
if (!this.isPlay) {
this.$refs.audio.play();
} else {
this.$refs.audio.pause();
}
this.isPlay = !this.isPlay;
},
// 当currentTime更新时会触发timeupdate事件
updateTime(e) {
this.currentTime = e.target.currentTime;
// console.log("currentTime " + this.currentTime);
},
progressTouchStart(e) {
this.touch.initiated = true; // 标志位 表示初始化
this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
console.log("start", this.touch.startX);
},
progressTouchMove(e) {
console.log("move");
if (!this.touch.initiated) {
return;
}
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth =
this.$refs.progressBar.clientWidth - progressBtnWidth;
const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
// 滑动中进度滑动条和滑动进度图标的当前位置
const offsetWidth = Math.min(
barWidth,
Math.max(0, this.touch.left + deltaX)
);
this._offset(offsetWidth);
},
progressTouchEnd() {
console.log("end");
this.touch.initiated = false;
this._triggerPercent();
},
progressClick(e) {
// getBoundingClientRect用于获取某个元素相对于视窗的位置集合
const rect = this.$refs.progressBar.getBoundingClientRect();
// pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
const offsetWidth = e.pageX - rect.left;
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth);
// 计算当前播放时间
this._triggerPercent();
},
// 计算当前时间
_triggerPercent() {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth =
this.$refs.progressBar.clientWidth - progressBtnWidth;
// 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
const percent = this.$refs.progress.clientWidth / barWidth;
// 当前播放时长 = 总时长 * 播放比例
const currentTime = this.totalTime * percent;
// 设置音频当前播放时间
this.$refs.audio.currentTime = currentTime;
},
// 计算拖动条的宽度和滑动偏移的距离
_offset(offsetWidth) {
this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
},
format(interval) {
interval = interval | 0; // 向下取整
const minute = (interval / 60) | 0;
const second = this._pad(interval % 60);
return `${minute}:${second}`;
},
_pad(num, n = 2) {
// 用0补位,补2位字符串长度
let len = num.toString().length;
while (len < n) {
num = "0" + num;
len++;
}
return num;
}
},
watch: {
percent(newPrecent) {
// 非滑动进度图标时间内
if (newPrecent >= 0 && !this.touch.initiated) {
// 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
const barWidth =
this.$refs.progressBar.clientWidth - progressBtnWidth;
// 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
const offsetWidth = newPrecent * barWidth;
// 进度滑动条偏移和滑动进度图标偏移
this._offset(offsetWidth);
}
}
}
};
</script>
<style lang="less" scoped>
.audio-wrapper {
background-color: tomato;
}
.audio1 {
margin: auto;
display: block;
width: 100%;
}
.fh-progress-wrapper {
display: flex;
align-items: center;
width: 80%;
margin: 0px auto;
padding: 10px 0;
.fh-time {
color: #fff;
font-size: 12px;
flex: 0 0 30px;
line-height: 30px;
width: 30px;
&.fh-time-l {
text-align: left;
}
&.fh-time-r {
text-align: right;
}
}
.fh-progress-bar-wrapper {
flex: 1;
}
}
.fh-progress-bar {
height: 30px;
.fh-bar-inner {
position: relative;
top: 13px;
height: 4px;
background: rgba(0, 0, 0, 0.3);
.fh-progress {
position: absolute;
height: 100%;
background: #41b883;
}
.fh-progress-btn-wrapper {
position: absolute;
left: -7px;
top: -13px;
width: 30px;
height: 30px;
.fh-progress-btn {
position: relative;
top: 7px;
left: 7px;
box-sizing: border-box;
width: 16px;
height: 16px;
border: 3px solid #fff;
border-radius: 50%;
background: #41b883;
}
}
}
}
.isPlayOrPause {
text-align: center;
font-size: 60px;
}
</style>