通过微信小程序实现一个音乐播放器功能,功能如下,实现效果图如下
- 最终可以去github克隆,看最终效果
https://github.com/MrZHLF/wx-music.git
- 歌词联动
- 播放,暂停,切换上一首和下一首
- 全局背景音乐播放
- 歌词播放选择状态
- 拖拽播放进度
- 保存播放歌曲当前记录
通过分析这个页面,可以把这个页面拆分成三个组件,分别对应歌词组件,进度条组件和下面切换按钮组件
页面布局
<view class="player-container" style="background:url({{picUrl}}) center/cover no-repeat"></view>
<view class="player-mask"></view>
<view class="player-info">
<view class="player-disc {{isPlaying ? 'play' : ''}}" bindtap="onChangeLyriShow" hidden="{{isLyricShow}}">
<image class="player-img rotation {{isPlaying?'':'rotation-paused'}}" src="{{picUrl}}"></image>
</view>
<!-- 控制面板 -->
<view class="control">
<text class="iconfont icon-shangyishoushangyige" bind:tap="onPrev"></text>
<text class="iconfont {{isPlaying ? 'icon-zanting1' : 'icon-bofang1'}}" bind:tap="togglePlaying"></text>
<text class="iconfont icon-xiayigexiayishou" bind:tap="onNext"></text>
</view>
</view>
在js文件中定义一写变量值
picUrl:"", 封面图
isPlaying:false, //false不播放,true播放
let musiclist = []
// 正在播放歌曲的index
let nowPlayingIndex = 0
//获取全局位移背景音乐播放器
const backgroundAudioManager = wx.getBackgroundAudioManager()
通过接口请求数据musicId
是从列表传过来的,backgroundAudioManager
是微信小程序提供的一个全局的背景音乐api,通过设置对应的参数值,最重要的就是设置src,表示这个首歌曲的地址,_loadMusicDetail
需要在
_loadMusicDetail(musicId) {
let music = musiclist[nowPlayingIndex]
wx.setNavigationBarTitle({
title:music.name,
})
this.setData({
picUrl: music.al.picUrl,
isPlaying:false
})
wx.showLoading({
title: '歌曲加载中...',
})
wx.cloud.callFunction({
name:'music',
data:{
musicId,
$url:'musicUrl'
}
}).then((res)=>{
let result = JSON.parse(res.result)
//设置全局背景音乐播放器
if(result.data[0].url == null) {
wx.showToast({
title: '无权限播放',
})
return
}
if(!this.data.isSame) {
// 如果不是同一首歌曲的话,设置播放属性,
backgroundAudioManager.src = result.data[0].url
backgroundAudioManager.title = music.name
backgroundAudioManager.coverImgUrl = music.al.picUrl
backgroundAudioManager.singer = music.ar[0].name
backgroundAudioManager.epname = music.al.name
}
this.setData({
isPlaying: true
})
wx.hideLoading()
// 加载歌词
},
通过点击暂停或者播放来切换isPlaying
状态值
根据这个值来动态的切换播放按钮和暂停按钮已经图片旋转功能,
togglePlaying(){
// 播放事件
if(this.data.isPlaying) {
//正在播放,点击暂停
backgroundAudioManager.pause()
} else {
// 点击播放
backgroundAudioManager.play()
}
this.setData({
isPlaying: !this.data.isPlaying
})
},
点击切换上一首
根据nowPlayingIndex
来判断当前的索引值,点击的时候-1,当点击到第一首的时候,再次点击,选择的是最后的一首
onPrev(){
// 上一首
nowPlayingIndex--
if(nowPlayingIndex<0) {
// 播放最后一个
nowPlayingIndex=musiclist.length -1
}
this._loadMusicDetail(musiclist[nowPlayingIndex].id)
},
点击切换下一首
每次点击的时候,都索引值+1,判断代当前的索引值如果等会数据的长度,点击的时候,让索引值赋值为0,从0开始再次点击播放
onNext(){
// 下一首
nowPlayingIndex++
if(nowPlayingIndex===musiclist.length) {
// 如果切换了最后一首之后,在切换,返回第一个
nowPlayingIndex=0
}
this._loadMusicDetail(musiclist[nowPlayingIndex].id)
},
开发进度条功能
在进度条这个功能,需要拿到每一首歌曲的总时间,根据播放时间,滑动的距离,如果当歌曲播放完之后,自动切换下一首歌曲。
这是一个抽离出来的组件,如果想使用,需要在父组件引入
页面布局开发
在这个components组件中,在data定义好开始时间和总时间和播放进度已经距离
data: {
showTime:{
currentTime:"00:00",
totalTime: "00:00"
},
movableDis:0,
progress:0, //进度
},
<view class="container">
<text class="time">{{showTime.currentTime}}</text>
<!-- 滑动 -->
<view class="control">
<movable-area class="movable-area">
<movable-view direction="horizontal" class="movable-view" damping="1000" x="{{movableDis}}" bindchange='onChange' bindtouchend="onTouchEnd"></movable-view>
</movable-area>
<progress stroke-width="4" backgroundColor="#969696" activeColor="#fff" percent="{{progress}}"></progress>
</view>
<text class="time">{{showTime.totalTime}}</text>
</view>
在父组件引入使用
首先在json文件引入
{
"usingComponents": {
"x-progress-bar": "/components/progress-bar/progress-bar"
}
}
然后使用
<!-- 进度条 -->
<view class="progress-bar">
<x-progress-bar ></x-progress-bar>
</view>
获取进度条的宽度
因为每个手机型号不同,所以要动态获取宽度,以便于后面的计算
- 这个方法在已进入到页面就要调用,
ready
调用,
_getMovableDis(){
//获取宽度
const query = this.createSelectorQuery()
query.select('.movable-area').boundingClientRect()
query.select('.movable-view').boundingClientRect()
query.exec((rect) =>{
movableAreaWidth = rect[0].width
movableViewWidth=rect[1].width
})
},
1.计算除每一首音乐的歌曲的时间,定义一个用于接受歌曲音乐的总时长和获取全局背景音乐的方法
const backgroundAudioManager = wx.getBackgroundAudioManager() //全局背景音乐
let duration = 0 // 当前歌曲的总时长,以秒为单位
_setTime(){
//算播放总时长
duration= backgroundAudioManager.duration //获取播放总时长
const durationFmt=this._dateFormat(duration)
this.setData({
['showTime.totalTime']: `${durationFmt.min}:${durationFmt.sec}`
})
},
计算的时间都是秒,这个时候需要转换成分钟
_dateFormat(sec){
//格式化时间
const min = Math.floor(sec/60) //分钟
sec = Math.floor(sec % 60) //秒
return {
'min': this._parse0(min),
'sec': this._parse0(sec)
}
},
有的我们是希望格式是```00:00``这种格式,这个时候我们需要对个位数补0
_parse0(sec) {
// 补0
return sec < 10 ? '0' + sec : sec
}
时间计算完之后,需要在backgroundAudioManager.onCanplay
调用,这个时候需要对这个时间判断一下,有个别机型会返回undefined
,如果等于undefined
的时候。需要过一秒钟的时候重新在加载一次,解决时间无法出来的问题
backgroundAudioManager.onCanplay(() => {
// 监听背景音频进入可播放状态事件。 但不保证后面可以流畅播放
if (typeof backgroundAudioManager.duration != 'undefined') {
//获取音频总时间
this._setTime()
} else {
setTimeout(()=>{
this._setTime()
},1000)
}
})
进度条移动
做到这个的时候,我们也拿到的歌曲的每一条的总时间,这个时候我们就要开始播放音乐的时候,开始播放的时间也要计算出来,并且进度条也要随着音乐而加载进度
backgroundAudioManager.onTimeUpdate(() => {
if (!isMoving){
const currentTime = backgroundAudioManager.currentTime //当前播放进度时间
const duration = backgroundAudioManager.duration //总时长
const sec = currentTime.toString().split('.')[0]
if (sec != currentSec) {
// 判断时间是否有想等的
const currentFmt = this._dateFormat(currentTime)
this.setData({
movableDis: (movableAreaWidth - movableViewWidth) * currentTime / duration,
progress: currentTime / duration * 100,
['showTime.currentTime']: `${currentFmt.min}:${currentFmt.sec}`
})
currentSec = sec
}
}
})
拖拽进度条,并且音乐随着播放
手拖拽的时候,会触发touch
事件,根据这个事件,计算出X坐标,赋值给progress
,
onChange(event){
// 移动
if(event.detail.source=='touch') {
this.data.progress=event.detail.x / (movableAreaWidth-movableViewWidth) * 100
this.data.movableDis =event.detail.x
isMoving=true
}
},
当手离开的时候,这个时候我只需要调用backgroundAudioManager.seek
这个api即刻
onTouchEnd(){
// 松开
const currentTimeFmt = this._dateFormat(Math.floor(backgroundAudioManager.currentTime))
this.setData({
progress:this.data.progress,
movableDis:this.data.movableDis,
['showTime.currentTime']: currentTimeFmt.min + ':' + currentTimeFmt.sec
})
backgroundAudioManager.seek(duration*this.data.progress / 100)
isMoving=false
},
歌词播放滚动
页面布局
<scroll-view hidden="{{isLyricShow}}" class="lyric-scroll" scroll-y scroll-top="{{scrollTop}}" scroll-with-animation="true">
<view class="lyric-panel">
<block wx:for="{{lrcList}}" wx:key="item">
<view class="lyric {{index==nowLyricIndex?'hightlight-lyric': ''}}">{{item.lrc}}</view>
</block>
</view>
</scroll-view>
这个时候需要在父组件,把请求的接口数据,传递给歌词组件,当页面一进来的时候,就需要调用observers
初始化,
observers:{
lyric(lrc) {
if (lrc =='暂无歌词') {
this.setData({
lrcList:[
{
lrc,
time:0
}
],
nowLyricIndex:-1
})
} else {
this._parseLyric(lrc)
}
}
},
解析歌曲,
// 解析歌词
_parseLyric(sLyric) {
let line = sLyric.split('\n')
let _lrcList=[]
line.forEach((elem) =>{
let time = elem.match(/\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g)
if(time != null) {
let lrc = elem.split(time)[1] //获取到歌词
let timeReg = time[0].match(/(\d{2,}):(\d{2})(?:\.(\d{2,3}))?/) //获取到时间
// 吧时间转换成秒
let time2Senconds = parseInt(timeReg[1]) * 60 + parseInt(timeReg[2]) + parseInt(timeReg[3]) / 1000
_lrcList.push({
lrc,
time: time2Senconds
})
}
})
this.setData({
lrcList: _lrcList
})
}
通过解析我们解析出我需要的格式
- 通过歌词解析成我们需要的格式之后,然后让歌词高亮
update(currentTime){
// 歌词高亮 从父组件拿到值
let lrcList = this.data.lrcList
if (lrcList.length == 0) {
return
}
// 歌词滚动
if (currentTime > lrcList[lrcList.length-1].time) {
if(this.data.nowLyricIndex!=-1) {
this.setData({
nowLyricIndex:-1,
scrollTop:lrcList.length * lyricHeight
})
}
}
for(let i=0,len=lrcList.length;i<len;i++) {
if (currentTime <= lrcList[i].time) {
this.setData({
nowLyricIndex:i-1,
scrollTop: (i - 1) * lyricHeight
})
break
}
}
},
- 因为我们在css样式中写的rpx,但是页面渲染的是px,所以我们要计算1rpx的大小
lifetimes:{
ready(){
wx.getSystemInfo({
success(res) {
// 计算除1rpx的大下
lyricHeight = res.screenWidth / 750 * 64
},
})
}
},