vue2实现歌曲播放和歌词滚动效果
需求:需要实现歌词滚动效果。
思路:通过js+css的transform属性完成。
难点:主要就是需要知道正在播放的歌词是那句,然后对正在播放的歌词进行变色和放大,最难的就是让高亮歌词随着歌曲播放滚动。
1.先看效果图
2.处理歌词格式(项目中有后端兄弟实现转换的可以省略)
// 处理歌词格式
parseLrc(musicLrc) {
const lines = musicLrc?.split(`\n`);
let lrcList = lines.map((line) => {
let [time, words] = line?.split("]") ?? [null, null];
return {
time: this.parseTime(time.substring(1)) || null,
words: words || null,
};
});
let lrcListRes = lrcList.filter((v) => {
return v.name !== null && v.words !== null;
});
// this.computingTime();
return lrcListRes;
},
parseTime(t) {
const part = t?.split(":");
return Number(part[0] * 60) + Number(part[1]);
},
歌词格式一般都是数组对象,对象的key各位可以自己根据需要命名,主要是思路。思路最重要!思路最重要!思路最重要!
我这里的数据格式如下图
3.利用audio的timeupdate的事件来进行计算歌词是否高亮以及偏移量
// 添加audio事件
autoDown() {
const that = this;
var audio = document.getElementById("myAudio");
audio.addEventListener("ended", function () {
that.switchingBtn("down");
});
audio.addEventListener("timeupdate", function () {
that.computingTime();
});
},
// 计算播放时间对应的下标
computingTime() {
let arr = this.selectedFiles[this.playIndex].lrcList || [];
let currentTime = document.getElementById("myAudio")?.currentTime || 0;
let index = arr.findIndex((e) => currentTime < e.time) - 1;
this.currentIndex = index >= 0 ? index : arr.length - 1;
},
// 计算偏移量(保证高亮的歌词在中间)
computingOffset(index) {
// 外部大盒子高度
let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;
// 歌词总高度
let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;
// 每个li高度
let musicLrcLiHeight = this.$refs.musicLrc_item?.clientHeight || 22;
// 歌词偏移高度
let offsetHenght =
index * musicLrcLiHeight + musicLrcLiHeight / 2 - musicLrcBoxHeight / 2;
// 最大偏移高度
let offsetMax = musicLrcHeight - musicLrcBoxHeight + 10;
if (offsetHenght < 0) {
offsetHenght = 0;
}
// if (offsetHenght > offsetMax) {
// offsetHenght = offsetMax;
// }
this.$refs.musicLrc_bady.style.transform = `translateY(-${offsetHenght}px)`;
},
注意:这里的computingTime和computingOffset两个事件是核心代码!!!
4.整个demo源码
<template>
<div class="h100 dis_sb bs">
<div class="music bg-fff bs">
<div class="p10 bs mb10" style="height: 40px">
<el-button type="text" size="small" @click="triggerFileInput">
选择歌曲
</el-button>
<span class="f12 ml10">请先选择本地音乐!!!</span>
<input
ref="audioInput"
style="display: none; height: 10px"
type="file"
@change="handleFileSelect"
multiple
accept="audio/*"
/>
</div>
<div class="bs p10" v-if="selectedFiles?.length > 0" style="height: 60px">
<audio
id="myAudio"
class="audio"
controls
:src="fileUrl || selectedFiles[0]?.url"
autoplay
/>
<div class="tac bs" style="line-height: 20px">
<el-button type="text" size="small" @click="switchingBtn('up')">
上一曲
</el-button>
<el-button type="text" size="small" @click="togglePlay">
{{ playing ? "暂停" : "播放" }}
</el-button>
<el-button type="text" size="small" @click="switchingBtn('down')">
下一曲
</el-button>
</div>
</div>
<div v-if="selectedFiles?.length > 0" class="p10 music_body">
<ul class="main bs mt20">
<li
:class="[index == playIndex ? 'main_item_action li' : 'li']"
v-for="(file, index) in selectedFiles"
:key="index"
>
<p @click="choose(file, index)">{{ file.name }}</p>
</li>
</ul>
</div>
</div>
<div class="bs h100 p10 musicLrc" ref="musicLrc">
<ul class="musicLrc_bady tac f14" ref="musicLrc_bady">
<li
v-for="(v, i) in selectedFiles[playIndex]?.lrcList"
:key="i"
ref="musicLrc_item"
>
<p :class="[i == currentIndex ? 'musicLrc_action ' : '']">
{{ v.words }}
</p>
</li>
<div
v-if="selectedFiles[playIndex]?.lrcList?.length == 0"
class="tac bs"
style="margin: auto; padding-top: 30px; color: #ccc"
>
暂无歌词
</div>
</ul>
</div>
</div>
</template>
<script>
import musicList from "./musicList.js";
export default {
data() {
return {
selectedFiles: [],
lrcList: [],
fileUrl: null,
playing: false,
isPlay: false,
playIndex: 0,
currentTime: 0,
currentIndex: null,
};
},
created() {
musicList.forEach((e) => {
if (e.lrc) {
e.lrcList = this.parseLrc(e.lrc);
} else {
e.lrcList = [];
}
});
this.selectedFiles = JSON.parse(JSON.stringify(musicList));
},
watch: {
selectedFiles: {
handler(newVal, oldVal) {
if (newVal?.length > 4) {
this.$nextTick(() => {
this.fileUrl = newVal[0]?.url;
this.playing = true;
this.playIndex = 0;
});
}
},
deep: true,
},
$route: {
handler(newVal, oldVal) {
if (newVal?.path !== "/music") {
this.$nextTick(() => {
this.playing = false;
});
}
},
deep: true,
},
currentIndex: {
handler(newVal, oldVal) {
if (newVal > 0) {
setTimeout(() => {
this.computingOffset(newVal);
}, 150);
} else {
this.computingOffset(0);
}
},
deep: true,
},
},
mounted() {
this.$nextTick(() => {
this.autoDown();
});
},
methods: {
// 添加本地音乐
handleFileSelect(event) {
const files = event.target.files;
this.selectedFiles = JSON.parse(JSON.stringify(musicList));
this.fileUrl = null;
for (let i = 0; i < files?.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = (e) => {
const fileObj = {
name: file.name,
url: e.target.result,
// lrc: null,
lrcList: [],
};
this.selectedFiles.push(fileObj);
};
reader.readAsDataURL(file);
}
},
triggerFileInput() {
this.$refs.audioInput.click();
},
// 选择播放歌曲
choose(v, i) {
const that = this;
that.playing = true;
that.fileUrl = that.selectedFiles[i]?.url;
that.playIndex = i;
},
// 判断播放状态
togglePlay() {
const that = this;
var audio = document.getElementById("myAudio");
if (audio.paused) {
audio.play();
that.playing = true;
} else {
audio.pause();
that.playing = false;
}
},
// 添加audio事件
autoDown() {
const that = this;
var audio = document.getElementById("myAudio");
audio.addEventListener("ended", function () {
that.switchingBtn("down");
});
audio.addEventListener("timeupdate", function () {
that.computingTime();
});
},
// 播放歌曲
playAudio() {
var audio = document.getElementById("myAudio");
audio.play();
this.currentIndex = 0;
},
// 切换歌曲
switchingBtn(v) {
const that = this;
that.playing = true;
const length = this.selectedFiles?.length || 0;
if (v === "down") {
this.playIndex = (this.playIndex + 1) % length;
} else {
this.playIndex = (this.playIndex - 1 + length) % length;
}
that.fileUrl = that.selectedFiles[that.playIndex]?.url;
setTimeout(() => {
that.playAudio();
}, 150);
},
// 处理歌词格式
parseLrc(musicLrc) {
const lines = musicLrc?.split(`\n`);
let lrcList = lines.map((line) => {
let [time, words] = line?.split("]") ?? [null, null];
return {
time: this.parseTime(time.substring(1)) || null,
words: words || null,
};
});
let lrcListRes = lrcList.filter((v) => {
return v.name !== null && v.words !== null;
});
// this.computingTime();
return lrcListRes;
},
parseTime(t) {
const part = t?.split(":");
return Number(part[0] * 60) + Number(part[1]);
},
// 计算播放时间对应的下标
computingTime() {
let arr = this.selectedFiles[this.playIndex].lrcList || [];
let currentTime = document.getElementById("myAudio")?.currentTime || 0;
let index = arr.findIndex((e) => currentTime < e.time) - 1;
this.currentIndex = index >= 0 ? index : arr.length - 1;
},
// 计算偏移量(保证高亮的歌词在中间)
computingOffset(index) {
// 外部大盒子高度
let musicLrcBoxHeight = this.$refs.musicLrc?.clientHeight;
// 歌词总高度
let musicLrcHeight = this.$refs.musicLrc_bady?.clientHeight;
// 每个li高度
let musicLrcLiHeight = this.$refs.musicLrc_item?.clientHeight || 22;
// 歌词偏移高度
let offsetHenght =
index * musicLrcLiHeight + musicLrcLiHeight / 2 - musicLrcBoxHeight / 2;
// 最大偏移高度
let offsetMax = musicLrcHeight - musicLrcBoxHeight + 10;
if (offsetHenght < 0) {
offsetHenght = 0;
}
// if (offsetHenght > offsetMax) {
// offsetHenght = offsetMax;
// }
this.$refs.musicLrc_bady.style.transform = `translateY(-${offsetHenght}px)`;
},
},
};
</script>
<style scoped>
.music {
width: calc(50% - 5px);
height: 100%;
border-radius: 10px;
}
#myAudio {
width: 100%;
height: 30px;
}
.music_body {
height: calc(100% - 126px);
overflow-x: hidden;
overflow-y: auto;
}
.main {
.li {
border: 1px solid #eee;
border-radius: 5px;
padding: 0 10px;
margin-bottom: 10px;
box-sizing: border-box;
line-height: 30px;
font-size: 12px;
cursor: grab;
color: #666;
}
.main_item_action {
border: 1px solid #409eff;
color: #409eff;
}
}
.musicLrc {
width: calc(50% - 5px);
height: 100%;
background-color: rgb(0, 0, 0);
border-radius: 10px;
color: #ccc;
line-height: 22px;
/* overflow: hidden; */
transform: translateY();
overflow-x: hidden;
overflow-y: auto;
}
.musicLrc_bady {
li {
transition: 0.8s;
}
}
.musicLrc_action {
transform: scale(1.5);
color: #409eff;
}
</style>