当前位置: 首页 > news >正文

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博客

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 如何提升网站的收录率?
  • Linux 进程信号
  • 【JAVA基础】StringUtils.isEmpty、StringUtils.isBlank()、Objects.isNull()三者区别
  • 基因组学中的深度学习
  • C++学习笔记(8)
  • 单点登录及登录相关功能1
  • 代码随想录:279. 完全平方数
  • Qt 模仿企业微信图标实现按钮图片文字上下结构
  • MDC实现日志链路追踪
  • 高性能计算应用优化之代码实现调优(一)
  • TypeScript Agenda异常 undefined (reading ‘collection‘)
  • word中怎么快速选中光标之前或之后的全部内容?
  • 二、Maven工程的构建--JavaSEJavaEE
  • 软考高级:系统架构设计师——软件架构设计 Chapter 笔记
  • Redis持久化机制—RDB与AOF
  • “大数据应用场景”之隔壁老王(连载四)
  • Angular数据绑定机制
  • Apache Pulsar 2.1 重磅发布
  • extract-text-webpack-plugin用法
  • Fabric架构演变之路
  • gitlab-ci配置详解(一)
  • Js基础知识(一) - 变量
  • js写一个简单的选项卡
  • Linux编程学习笔记 | Linux多线程学习[2] - 线程的同步
  • magento2项目上线注意事项
  • SpringCloud集成分布式事务LCN (一)
  • vue 配置sass、scss全局变量
  • 初识 webpack
  • 好的网址,关于.net 4.0 ,vs 2010
  • 近期前端发展计划
  • 深入浏览器事件循环的本质
  • 世界上最简单的无等待算法(getAndIncrement)
  • Spring第一个helloWorld
  • 曾刷新两项世界纪录,腾讯优图人脸检测算法 DSFD 正式开源 ...
  • 智能情侣枕Pillow Talk,倾听彼此的心跳
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • # Java NIO(一)FileChannel
  • (1)Jupyter Notebook 下载及安装
  • (5)STL算法之复制
  • (C语言)字符分类函数
  • (javascript)再说document.body.scrollTop的使用问题
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一
  • (阿里云在线播放)基于SpringBoot+Vue前后端分离的在线教育平台项目
  • (创新)基于VMD-CNN-BiLSTM的电力负荷预测—代码+数据
  • (翻译)Entity Framework技巧系列之七 - Tip 26 – 28
  • (附源码)spring boot车辆管理系统 毕业设计 031034
  • (附源码)ssm高校升本考试管理系统 毕业设计 201631
  • (一)Linux+Windows下安装ffmpeg
  • (原创)boost.property_tree解析xml的帮助类以及中文解析问题的解决
  • (转)人的集合论——移山之道
  • .bashrc在哪里,alias妙用
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
  • .NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库