本文主要是介绍UniApp实现漂亮的音乐歌词滚动播放效果,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
在现代的音乐播放应用中,歌词的展示和滚动播放已经成为了一个非常常见的功能。今天,我们将通过UniApp来实现一个漂亮的歌词滚动播放功能。我们将使用UniApp提供的组件和API来完成这个任务。
页面结构
在页面的模板部分,我们需要创建一个音频播放器和歌词展示区域。使用<scroll-view>
组件来实现歌词的滚动效果。
<template><view class="audio-container"><!-- 音频播放器 --><view class="audio-player"><audio :src="audioSrc" @timeupdate="updateTime" @ended="audioEnded"></audio><view class="controls"><button @click="playAudio">播放</button><button @click="pauseAudio">暂停</button></view><view class="time">{{ currentTime }} / {{ duration }}</view></view><!-- 歌词展示区域 --><scroll-view class="lyrics" scroll-y :scroll-top="scrollTop"><view v-for="(line, index) in lyrics" :key="index" :class="{ active: currentLineIndex === index }">{{ line.text }}</view></scroll-view></view>
</template>
脚本逻辑
在脚本部分,我们需要处理音频的播放、暂停、时间更新等事件,并根据当前播放时间更新歌词的显示和滚动位置。
<script>
export default {data() {return {audioSrc: 'https://example.com/audio.mp3', // 音频文件地址lyrics: [{ time: 0, text: '第一行歌词' },{ time: 5000, text: '第二行歌词' },{ time: 10000, text: '第三行歌词' },// 更多歌词行...],currentTime: '00:00', // 当前播放时间duration: '00:00', // 音频总时长currentLineIndex: 0, // 当前高亮的歌词行索引scrollTop: 0, // 歌词滚动位置};},methods: {playAudio() {const audio = document.querySelector('audio');audio.play();},pauseAudio() {const audio = document.querySelector('audio');audio.pause();},updateTime(event) {const audio = event.target;this.currentTime = this.formatTime(audio.currentTime);this.duration = this.formatTime(audio.duration);this.updateLyrics(audio.currentTime * 1000); // 转换为毫秒},audioEnded() {this.currentTime = '00:00';this.currentLineIndex = 0;this.scrollTop = 0;},updateLyrics(currentTime) {for (let i = 0; i < this.lyrics.length; i++) {if (currentTime >= this.lyrics[i].time) {this.currentLineIndex = i;} else {break;}}this.scrollLyrics();},scrollLyrics() {const lineHeight = 30; // 每行歌词的高度this.scrollTop = this.currentLineIndex * lineHeight;},formatTime(time) {const minutes = Math.floor(time / 60);const seconds = Math.floor(time % 60);return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;},},
};
</script>
样式设计
在样式部分,我们可以设计音频播放器和歌词展示区域的样式,使其看起来更加美观。
<style scoped>
.audio-container {padding: 20px;
}.audio-player {margin-bottom: 20px;
}.controls {margin-bottom: 10px;
}.time {font-size: 14px;color: #666;
}.lyrics {height: 300px;overflow-y: auto;border: 1px solid #ccc;padding: 10px;font-size: 16px;line-height: 1.5;text-align: center;
}.lyrics view {transition: color 0.3s ease;
}.lyrics .active {color: #ff6600;font-weight: bold;
}
</style>
运行效果
通过以上步骤,你可以在UniApp中实现一个漂亮的音乐歌词滚动播放效果。运行项目后,你应该能够看到一个带有播放、暂停按钮的音频播放器,以及随着音乐播放自动滚动的歌词。
app体验地址
项目开源地址:imovie: 爱电影小程序uni-app
歌词解析
网络上拿到的歌词,可能是类似如下格式:
{"songStatus":1,"lyricVersion":3,"lyric":"[by:有个陷阱他们早就已经沦陷]\n\n[00:09.23]\n[00:20.20]花的心藏在蕊中\n[00:23.80]空把花期都错过\n[00:27.26]\n[00:29.52]你的心忘了季节\n[00:33.12]从不轻易让人懂\n[00:37.24]\n[00:38.81]为何不牵我的手\n[00:42.49]共听日月唱首歌\n[00:46.09]\n[00:47.46]黑夜又白昼\n[00:49.72]黑夜又白昼\n[00:51.73]人生为欢有几何\n[00:55.54]\n[00:57.00]春去春会来\n[01:01.72]花谢花会再开\n[01:06.08]只要你愿意\n[01:08.39]只要你愿意\n[01:10.41]让梦划向你心海\n[01:15.65]春去春会来\n[01:20.32]花谢花会再开\n[01:24.73]只要你愿意\n[01:27.06]只要你愿意\n[01:29.02]让梦划向你心海\n[01:33.43]\n[02:12.13]花瓣泪飘落风中\n[02:15.73]虽有悲意也从容\n[02:19.33]\n[02:21.41]你的泪晶莹剔透\n[02:25.04]心中一定还有梦\n[02:29.30]\n[02:30.73]为何不牵我的手\n[02:34.38]同看海天成一色\n[02:39.36]潮起又潮落\n[02:41.68]潮起又潮落\n[02:43.68]送走人间许多愁\n[02:48.28]\n[02:48.99]春去春会来\n[02:53.61]花谢花会再开\n[02:57.98]只要你愿意\n[03:00.35]只要你愿意\n[03:02.32]让梦划向你心海\n[03:07.58]春去春会来\n[03:12.32]花谢花会再开\n[03:16.58]只要你愿意\n[03:18.95]只要你愿意\n[03:21.01]让梦划向你心海\n[03:26.23]只要你愿意\n[03:28.29]只要你愿意\n[03:30.26]让梦划向你心海\n[03:35.06]\n","code":200}
需要对其解析,解析为类似以下的格式:
lyrics: [{ time: 0, text: '第一行歌词' },{ time: 5000, text: '第二行歌词' },{ time: 10000, text: '第三行歌词' },// 更多歌词行...],
解析方法:
/*** 歌词解析* @param {lrcContent} string - 歌词内容* @returns {lyrics} 对象数组*/
function parseLyric(lrcContent) {const lines = lrcContent.split('\n');const lyrics = [];lines.forEach(line => {const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\]/);if (match) {const minutes = parseInt(match[1]);const seconds = parseInt(match[2]);const milliseconds = parseInt(match[3]);const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds;// 提取歌词文本const text = line.replace(/\[\d{2}:\d{2}\.\d{2,3}\]/g, '').trim();lyrics.push({ time, text });}});return lyrics;
}
完成audio组件源码
<template><view class="audio_container"><view class="audio-title"style="width: 100%; text-align: left; font-size: 36rpx;font-weight: bold;padding: 0rpx 0rpx; position: relative;"><uni-notice-bar single :scrollable="titleScroll" :size="titleFontSize":background-color="titleBackgroundColor" :color="titleColor" :speed="titleScrollSpeed" :text="title"class="uni-noticebar" style="padding: 0px; margin-bottom: 0px;"></uni-notice-bar><uni-fav v-show="isCollectBtn" :checked="isFavorited" class="favBtn" bgColor="#dddddd" bgColorChecked="#ffaa00" @click="handleCollec"style="color:#848484; position: absolute;top: 0rpx;right: 0px;"></uni-fav></view><view class="audio-subTitle":style="'font-size: '+subTitleFontSize+';font-weight: bold;padding: 0rpx 0rpx 4rpx 0rpx;position: relative;'"><uni-notice-bar single :scrollable="titleScroll" :size="titleFontSize":background-color="titleBackgroundColor" :color="subTitleColor" :speed="titleScrollSpeed":text="localSubTitle" class="uni-noticebar"></uni-notice-bar><uni-icons v-show="isShareBtn" @click="handleShare" type="redo" size="20"style="color:#848484;position: absolute;top: 0rpx;right: 0px;"></uni-icons></view><view><slider :backgroundColor='backgroundColor' :activeColor='activeColor' @change="handleSliderChange":value="sliderIndex" :max="maxSliderIndex" block-color="#343434" block-size="16" /></view><view style="padding: 0rpx 15rpx 0rpx 15rpx ; display: block; "><view style="float: left; font-size: 20rpx;color:#848484;">{{currentTimeText}}</view><view style="float: right;font-size: 20rpx;color:#848484;">{{totalTimeText}}</view></view><view style="margin-top: 70rpx;"><uni-grid :column="5" :showBorder="false" :square="false"><uni-grid-item><view class="uni-grid-icon"><image @tap="handleFastRewind" src="../../static/images/playlist.svg"style="width: 48rpx;height: 48rpx;top:6rpx;"></image></view></uni-grid-item><uni-grid-item><view class="uni-grid-icon"><image @tap="handleFastRewind" src="../../static/images/get-back.svg"style="width: 48rpx;height: 48rpx;top:6rpx;"></image></view></uni-grid-item><uni-grid-item><view class="uni-grid-icon"><image @tap="handleChangeAudioState" v-show="!isPlaying" src="../../static/images/play.svg"style="width: 48rpx;height: 48rpx;top:6rpx;"></image><image @tap="handleChangeAudioState" v-show="isPlaying" src="../../static/images/pause.svg"style="width: 48rpx;height: 48rpx;top:6rpx;"></image></view></uni-grid-item><uni-grid-item><view class="uni-grid-icon"><image @tap="handleFastForward" src="../../static/images/fast-forward.svg"style="width: 48rpx;height: 48rpx;top:6rpx;"></image></view></uni-grid-item><uni-grid-item><view class="uni-grid-icon"><image @tap="handleLoopPlay" src="../../static/images/Loop.svg"style="width: 48rpx;height: 48rpx; top:6rpx; "></image></view></uni-grid-item></uni-grid></view><view v-show="isShowLrc"><scroll-view class="lyrics" scroll-y :scroll-top="scrollTop" :current="currentLineIndex" ref="lyricsContainer" ><block v-for="(line, index) in lyrics" :key="index"><view :class="{ 'active': currentLineIndex === index }">{{ line.text }}</view></block></scroll-view></view></view>
</template>
<script>export default {name: 'my-audio',//audioPlay开始播放//audioPause停止播放//audioEnd音频自然播放结束事件//audioCanplay音频进入可以播放状态,但不保证后面可以流畅播放//change播放状态改变 返回值false停止播放 true开始播放//audioError 播放器错误//audioCollec 音频收藏emits: ['audioPlay', 'audioPause', 'audioEnd', 'audioCanplay', 'change', 'audioError','audioCollec'],props: {//标题文字title: {type: String,default: '空'},//标题默认字体大小titleFontSize: {type: Number,default: 35},//标题文字颜色titleColor: {type: String,default: '#303030'},//标题背景色titleBackgroundColor: {type: String,default: 'white'},//标题是否滚动titleScroll: {type: Boolean,default: false},//标题滚动速度titleScrollSpeed: {type: Number,default: 100},subTitle: {type: String,default: '空'},subTitleColor: {type: String,default: '#6C7996'},subTitleFontSize: {type: String,default: "30rpx"},//是否自动播放autoplay: {type: Boolean,default: false},//滑块左侧已选择部分的线条颜色activeColor: {type: String,default: '#7C7C7C'},//滑块右侧背景条的颜色backgroundColor: {type: String,default: '#E5E5E5'},//音频地址src: {type: [String, Array],default: ''},//是否倒计时isCountDown: {type: Boolean,default: false},//音乐封面audioCover: {type: String,default: ''},//是否显示收藏按钮isCollectBtn: {type: Boolean,default: false},//状态是否是已收藏isFavorited: {type: Boolean,default: false},//是否显示分享按钮isShareBtn: {type: Boolean,default: false},//是否显示歌词isShowLrc: {type: Boolean,default: false},//歌词信息lyrics: {type: [Array],default: []},},data() {return {totalTimeText: '00:00', //视频总长度文字currentTimeText: '00:00:00', //视频已播放长度文字isPlaying: false, //播放状态sliderIndex: 0, //滑块当前值maxSliderIndex: 100, //滑块最大值IsReadyPlay: false, //是否已经准备好可以播放了isLoop: false, //是否循环播放speedValue: [0.5, 0.8, 1.0, 1.25, 1.5, 2.0],speedValueIndex: 2,playSpeed: '1.0', //播放倍速 可取值:0.5/0.8/1.0/1.25/1.5/2.0currentLineIndex: 0,localSubTitle:this.subTitle,shortLrc:'',scrollTop: 0, // 初始滚动位置stringObject: (data) => {return typeof(data)},innerAudioContext: uni.createInnerAudioContext()}},watch: {subTitle(newVal) {this.localSubTitle = newVal;}},async mounted() {this.innerAudioContext.src = typeof(this.src) == 'string' ? this.src : this.src[0];if (this.autoplay) {if (!this.src) return console.error('src cannot be empty,The target value is string or array')// #ifdef H5var ua = window.navigator.userAgent.toLowerCase();if (ua.match(/MicroMessenger/i) == 'micromessenger') {const jweixin = require('../../utils/jweixin');jweixin.config({});jweixin.ready(() => {WeixinJSBridge.invoke('getNetworkType', {}, (e) => {this.innerAudioContext.play();})})}// #endif// #ifndef H5this.innerAudioContext.autoplay = true;// #endif}//音频播放事件this.innerAudioContext.onPlay(() => {this.isPlaying = true;this.$emit('audioPlay')this.$emit('change', {state: true});setTimeout(() => {this.maxSliderIndex = parseFloat(this.innerAudioContext.duration).toFixed(2);}, 100)});//音频暂停事件this.innerAudioContext.onPause(() => {this.$emit('audioPause');this.$emit('change', {state: false});});//音频自然播放结束事件this.innerAudioContext.onEnded(() => {this.isPlaying = !this.isPlaying;this.$emit('audioEnd');if (this.isLoop) {this.changePlayProgress(0);this.innerAudioContext.play();}});//音频进入可以播放状态,但不保证后面可以流畅播放this.innerAudioContext.onCanplay((event) => {this.IsReadyPlay = true;this.$emit('audioCanplay');let duration = this.innerAudioContext.duration;//console.log('总时长', duration)//将当前音频长度秒转换为00:00:00格式this.totalTimeText = this.getFormateTime(duration);this.maxSliderIndex = parseFloat(duration).toFixed(2);//console.log(this.getFormateTime(duration))//console.log('总时长1', this.totalTimeText)//防止视频无法正确获取时长setTimeout(() => {duration = this.innerAudioContext.duration;//将当前音频长度秒转换为00:00:00格式this.totalTimeText = this.getFormateTime(duration);this.maxSliderIndex = parseFloat(duration).toFixed(2);//console.log('总时长2', this.totalTimeText)}, 300)});//音频播放错误事件this.innerAudioContext.onTimeUpdate((res) => {this.sliderIndex = parseFloat(this.innerAudioContext.currentTime).toFixed(2);this.currentTimeText = this.getFormateTime(this.innerAudioContext.currentTime);//更新歌词const currentTime = this.innerAudioContext.currentTime * 1000; // 转换为毫秒this.updateLyrics(currentTime);});//音频播放错误事件this.innerAudioContext.onError((res) => {console.log(res.errMsg);console.log(res.errCode);this.$emit('change', {state: false});this.audioPause();this.$emit('audioError', res);});},methods: {//销毁innerAudioContext()实例audioDestroy() {console.log("audioDestroy")if (this.innerAudioContext) {if (this.isPlaying && !this.innerAudioContext.paused) {this.audioPause();}this.innerAudioContext.destroy();this.isPlaying = false;}},//点击变更播放状态handleChangeAudioState() {if(this.src ===''){uni.showToast({title: '无播放资源',icon: 'none',duration: 1000});return;}if (this.isPlaying && !this.innerAudioContext.paused) {this.audioPause();} else {this.audioPlay();}},//开始播放audioPlay() {this.$nextTick(() => {this.innerAudioContext.src = this.src;setTimeout(() => {this.innerAudioContext.play();this.isPlaying = true;}, 100); // 100毫秒});},//暂停播放audioPause() {this.innerAudioContext.pause();this.isPlaying = false;},//变更滑块位置handleSliderChange(e) {this.changePlayProgress(e.detail ? e.detail.value : e)},//更改播放倍速handleChageSpeed() {//获取播放倍速列表长度let speedCount = this.speedValue.length;//如果当前是最大倍速,从-1开始if (this.speedValueIndex == (speedCount - 1)) {this.speedValueIndex = -1;}//最新倍速序号this.speedValueIndex += 1;//获取最新倍速文字this.playSpeed = this.speedValue[this.speedValueIndex].toFixed(1);//暂停播放this.audioPause();//变更播放倍速this.innerAudioContext.playbackRate(this.speedValue[this.speedValueIndex]);//开始播放this.audioPlay();},//快退15秒handleFastRewind() {if (this.IsReadyPlay) {let value = parseInt(this.sliderIndex) - 15;this.changePlayProgress(value >= 0 ? value : 0);}},//快进15秒handleFastForward() {if (this.IsReadyPlay) {let value = parseInt(this.sliderIndex) + 15;this.changePlayProgress(value <= this.innerAudioContext.duration ? value : this.innerAudioContext.duration);}},//开启循环播放handleLoopPlay() {this.isLoop = !this.isLoop;if (this.isLoop) {uni.showToast({title: '已开启循环播放',duration: 1000});} else {uni.showToast({title: '取消循环播放',duration: 1000});}},//更改播放进度changePlayProgress(value) {this.innerAudioContext.seek(value);this.sliderIndex = value;this.currentTimeText = this.getFormateTime(value);},//秒转换为00:00:00getFormateTime(time) {let ms = time * 1000; // 1485000毫秒let date = new Date(ms);// 注意这里是使用的getUTCHours()方法,转换成UTC(协调世界时)时间的小时let hour = date.getUTCHours();// let hour = date.getHours(); 如果直接使用getHours()方法,则得到的时分秒格式会多出来8个小时(在国内开发基本都是使用的是东八区时间),getHours()方法会把当前的时区给加上。let minute = date.getMinutes();let second = date.getSeconds();let formatTime =`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}:${second.toString().padStart(2, '0')}`;return formatTime;},handleCollec() {this.$emit('audioCollec');},handleShare() {this.$emit('audioShare');},updateLyrics(currentTime) {for (let i = 0; i < this.lyrics.length; i++) {if (currentTime >= this.lyrics[i].time) {this.currentLineIndex = i;this.shortLrc = this.lyrics[i].text;this.localSubTitle = this.subTitle + ' : '+this.shortLrc} else {break;}}this.scrollLyrics();},scrollLyrics() {const lineHeight = 20; // 每行歌词的高度this.scrollTop = this.currentLineIndex * lineHeight;},},onLoad() {console.log("onLoad")},onUnload() {console.log("onUnload")this.audioDestroy()},onHide() {console.log("onHide")this.audioDestroy()},beforeDestroy() {console.log("beforeDestroy")this.audioDestroy()}}
</script><style lang="scss" scoped>.audio_container {box-shadow: 0 0 10rpx #c3c3c3;padding: 30rpx 20rpx 30rpx 20rpx;.audio-title {font-size: 28rpx;}.uni-noticebar {padding: 0px;padding-right: 50rpx;margin-bottom: 0px;display: inline-block;}.audio-subTitle {width: 100%;text-align: left;font-size: 40rpx;color: blue;}.speed-text {position: absolute;top: 0rpx;left: 30rpx;right: 0;color: #475266;font-size: 16rpx;font-weight: 600;}.uni-grid-icon {text-align: center;}.lyrics {margin-top: 20px;height: 660rpx; /* 设置歌词容器的高度 */// overflow: hidden; /* 隐藏溢出的歌词 */overflow-y: auto; /* 允许垂直滚动 */position: relative;font-size: 32rpx;line-height: 1.8;text-align: center;}.lyrics view {transition: color 1.2s ease; /* 添加平滑颜色变化效果 */}.lyrics .active {color: #00aa00;font-size: 45rpx;font-weight: bold;}}
</style>
总结
通过使用UniApp的组件和API,我们可以轻松实现音乐歌词的滚动播放效果。关键在于监听音频的播放时间,并根据时间更新歌词的显示和滚动位置。
这里面有个悬而未决的问题,就是这个滚动显示,有时候会滚动到最上方或最下方,导致在视野区域看不到。以下的处理,虽然简单, 但也粗暴。原因就出在这里:
scrollLyrics() {const lineHeight = 20; // 每行歌词的高度this.scrollTop = this.currentLineIndex * lineHeight;
},
如何让歌词能够根据进度居中显示?有知道的欢迎留言,感谢!
其他资源
vue实现歌词滚动_vue 实现一个歌词滚动效果-CSDN博客
这篇关于UniApp实现漂亮的音乐歌词滚动播放效果的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!