知識
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價值,我們在追求其視覺表現(xiàn)的同時,更側重于功能的便捷,營銷的便利,運營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實提升企業(yè)內部管理水平和效率。優(yōu)秀的程序為后期升級提供便捷的支持!
談談微信小程序仿網(wǎng)易云音樂有關播放的那些事兒
發(fā)表時間:2021-1-6
發(fā)布人:葵宇科技
瀏覽次數(shù):99
前言
筆者前端小兵一枚,在學習了一段時間的小程序后,決定親自動手做一款模仿一款手機軟件來練手,自己平常也熱愛音樂,并且發(fā)現(xiàn)各家的音樂平臺的小程序都比較簡單,于是就選擇了這個方向來進行模仿學習,在這個過程中也遇到了很多問題,在解決這些問題后,也有了一些收獲,今天就來和大家分享在這個小程序中,最難的音樂播放這一部分的種種問題和解決。
選擇這個項目,也是因為后端api有大佬提供了,需要數(shù)據(jù)的時候只用發(fā)起一些接口請求就可以了,比較適合像我這樣的初學者入門,只用寫一些簡單的前端邏輯就可以了。
由于播放頁面需要處理的事情較多(例如歌詞的處理與展示、進度條的快進快退等等),并且坑比較多,為了盡可能的描述清楚,所以本篇文章主要著重介紹和音樂播放有關的種種操作,有關于本項目其他頁面的詳情介紹,將放在后續(xù)文章進行詳細敘述,感謝各位讀者大大的理解。
項目界面預覽:
正式開始
有關于音樂播放的幾個接口請求中,幾乎都需要攜帶歌曲 id,在本項目的所有頁面中,播放頁面作為一個獨立的頁面存在,當別的頁面跳轉到播放頁面時,都會攜帶歌曲 id
接口封裝
本項目使用的接口請求有點多,為了方便,我將其封裝在utils
文件夾中的api.js
文件中,再在頁面中引用接口管理文件。
// method(HTTP 請求方法),網(wǎng)易云API提供get和post兩種請求方式
const GET = 'GET';
const POST = 'POST';
// 定義全局常量baseUrl用來存儲前綴
const baseURL = 'http://neteasecloudmusicapi.zhaoboy.com';
function request(method, url, data) {
return new Promise(function (resolve, reject) {
let header = { //定義請求頭
'content-type': 'application/json',
};
wx.request({
url: baseURL + url,
method: method,
data: method === POST ? JSON.stringify(data) : data,
header: header,
success(res) {
//請求成功
//判斷狀態(tài)碼---errCode狀態(tài)根據(jù)后端定義來判斷
if (res.data.code == 200) { //請求成功
resolve(res);
} else {
//其他異常
reject('運行時錯誤,請稍后再試');
}
},
fail(err) {
//請求失敗
reject(err)
}
})
})
}
const API = {
getSongDetail: (data) => request(GET, `/song/detail`, data), //獲取歌曲詳情
getSongUrl:(data) => request(GET, `/song/url`, data), //獲取歌曲路徑
};
module.exports = {
API: API
}
復制代碼
這里只展示了兩個在本頁面用到的請求API,在需要接口請求的頁面引入就可以使用了
const $api = require('../../utils/api.js').API;
音樂處理
頁面數(shù)據(jù)源
本頁面的使用到的data
數(shù)據(jù)源
data: {
musicId: -1,//音樂id
hidden: false, //加載動畫是否隱藏
isPlay: true, //歌曲是否播放
song: [], //歌曲信息
hiddenLyric: true, //是否隱藏歌詞
backgroundAudioManager: {}, //背景音頻對象
duration: '', //總音樂時間(00:00格式)
currentTime: '00:00', //當前音樂時間(00:00格式)
totalProcessNum: 0, //總音樂時間 (秒)
currentProcessNum: 0, //當前音樂時間(秒)
storyContent: [], //歌詞文稿數(shù)組,轉化完成用來在頁面中使用
marginTop: 0, //文稿滾動距離
currentIndex: 0, //當前正在第幾行
noLyric: false, //是否有歌詞
slide: false //進度條是否在滑動
},
復制代碼
其他頁面跳轉舉例:其他頁面跳轉到play頁面,攜帶musicId參數(shù)
//播放音樂
playMusic: function (e) {
let musicId = e.currentTarget.dataset.in.id // 獲取音樂id
// 跳轉到播放頁面
wx.navigateTo({
url: `../play/play?musicId=${musicId}`
})
},
復制代碼
onLoad生命周期
在play.js
的onLoad
生命周期函數(shù)中,通過options
拿到其他頁面?zhèn)鬟^來的musicId
這個參數(shù),并且調用play()
函數(shù)
/**
* 生命周期函數(shù)--監(jiān)聽頁面加載
*/
onLoad: function (options) {
const musicId = options.musicId //獲取到其他頁面?zhèn)鱽淼膍usicId
this.play(musicId) //調用play方法
},
復制代碼
播放函數(shù)
play()
函數(shù)需要一個形參:musicId
,這個形參非常重要,之后的接口請求都需要用到它
//播放音樂
play(musicId) {
const that = this;//將this對象復制給that
that.setData({
hidden: false,
musicId
})
app.globalData.musicId = musicId // 將當前音樂id傳到全局
// 通過musicId發(fā)起接口請求,請求歌曲詳細信息
//獲取到歌曲音頻,則顯示出歌曲的名字,歌手的信息,即獲取歌曲詳情;如果失敗,則播放出錯。
$api.getSongDetail({ ids: musicId }).then(res => {
// console.log('api獲取成功,歌曲詳情:', res);
if (res.data.songs.length === 0) {
that.tips('服務器正忙~~', '確定', false)
} else { //獲取成功
app.globalData.songName = res.data.songs[0].name
that.setData({
song: res.data.songs[0], //獲取到歌曲的詳細內容,傳給song
})
wx.request({ // 獲取歌詞
url: 'http://47.98.159.95/m-api/lyric',
data: {
id: musicId
},
success: res => {
if (res.data.nolyric || res.data.uncollected) { //該歌無歌詞,或者歌詞未收集
// console.log("無歌詞")
that.setData({
noLyric: true
})
}
else { //如果有歌詞,先調用sliceNull()去除空行,再調用parseLyric()格式化歌詞
that.setData({
storyContent: that.sliceNull(that.parseLyric(res.data.lrc.lyric))
})
}
}
})
// 通過音樂id獲取音樂的地址,請求歌曲音頻的地址,失敗則播放出錯,成功則傳值給createBackgroundAudioManager(后臺播放管理器,讓其后臺播放)
$api.getSongUrl({ id: musicId }).then(res => {
//請求成功
if (res.data.data[0].url === null) { //獲取出現(xiàn)錯誤出錯
that.tips('音樂播放出了點狀況~~', '確定', false)
} else {
// 調用createBackgroundAudioManager方法將歌曲url傳入backgroundAudioManager
that.createBackgroundAudioManager(res.data.data[0]);
}
})
.catch(err => {
//請求失敗
that.tips('服務器正忙~~', '確定', false)
})
}
})
.catch(err => {
//請求失敗
that.tips('服務器正忙~~', '確定', false)
})
},
復制代碼
總體大致的思路是:
- 先通過musicId請求歌曲的詳細信息(歌曲、歌手、歌曲圖片等信息)
- 在獲取成功后接著獲取該歌曲的歌詞信息(原歌詞請求地址有問題,導致這里換了一個接口,所以沒封裝,直接使用的
wx.request
做的請求),請求結果如果有歌詞,就將請求回來的歌詞數(shù)據(jù)設置到數(shù)據(jù)源中的storyContent
中,這時的歌詞還沒有經(jīng)過處理,之后還要處理一下歌詞,先調用parseLyric()
格式化歌詞,再調用sliceNull()
去除空行。 如果該歌沒有歌詞(情況比如:鋼琴曲這種純音樂無歌詞的、或者一些非常小眾的個人歌曲沒有上傳歌詞的),就設置數(shù)據(jù)源中的noLyric
為true
,設置了之后,頁面就會顯示:純音樂,無歌詞。點擊切換歌詞和封面
showLyric() {
this.setData({
hiddenLyric: !this.data.hiddenLyric
})
},
復制代碼
格式化歌詞
在請求回歌詞之后,還需要對歌詞進行分行處理
//格式化歌詞
parseLyric: function (text) {
let result = [];
let lines = text.split('\n'), //切割每一行
pattern = /\[\d{2}:\d{2}.\d+\]/g;//用于匹配時間的正則表達式,匹配的結果類似[xx:xx.xx]
// console.log(lines);
//去掉不含時間的行
while (!pattern.test(lines[0])) {
lines = lines.slice(1);
};
//上面用'\n'生成數(shù)組時,結果中最后一個為空元素,這里將去掉
lines[lines.length - 1].length === 0 && lines.pop();
lines.forEach(function (v /*數(shù)組元素值*/, i /*元素索引*/, a /*數(shù)組本身*/) {
//提取出時間[xx:xx.xx]
var time = v.match(pattern),
//提取歌詞
value = http://www.wxapp-union.com/v.replace(pattern,'');
// 因為一行里面可能有多個時間,所以time有可能是[xx:xx.xx][xx:xx.xx][xx:xx.xx]的形式,需要進一步分隔
time.forEach(function (v1, i1, a1) {
//去掉時間里的中括號得到xx:xx.xx
var t = v1.slice(1, -1).split(':');
//將結果壓入最終數(shù)組
result.push([parseInt(t[0], 10) * 60 + parseFloat(t[1]), value]);
});
});
// 最后將結果數(shù)組中的元素按時間大小排序,以便保存之后正常顯示歌詞
result.sort(function (a, b) {
return a[0] - b[0];
});
return result;
},
復制代碼
歌詞去除空白行
sliceNull: function (lrc) {
var result = []
for (var i = 0; i < lrc.length; i++) {
if (lrc[i][1] !== "") {
result.push(lrc[i]);
}
}
return result
},
復制代碼
-
再接著通過id去獲取歌曲的播放路徑,獲取到音頻的數(shù)據(jù)源后,則調用
createBackgroundAudioManager()
函數(shù),傳入剛剛獲取到的音頻數(shù)據(jù)源。(下文詳細介紹) -
如果其中的任意一個環(huán)節(jié)出現(xiàn)了問題,則會彈出提示信息,調用tips()函數(shù),并返回主頁
友好提示
- 播放頁面接口請求較多,并且調用頻繁,加上一些網(wǎng)絡波動,接口調用難免會出現(xiàn)一些失敗的情況,為了給用戶一些更好的反饋和提示,就使用了微信官方的顯示模態(tài)對話框
wx.showModal
,寫成了一個tips()
函數(shù),在想給提示對話框的時候,直接調用tips()
函數(shù)就可以,在出現(xiàn)錯誤之后,用戶點擊確定會觸發(fā)回調函數(shù)中的res.confirm
判斷,然后回到首頁,這里因為網(wǎng)易云手機app的導航在頭部,所以我是用的自定義組件做的導航,沒有使用tabBar
,頁面跳轉用的wx.navigateTo()
,如果大家使用了tabBar
,那么跳轉就應該換成wx.switchTab()
tips(content, confirmText, isShowCancel) { wx.showModal({ content: content, confirmText: confirmText, cancelColor: '#DE655C', confirmColor: '#DE655C', showCancel: isShowCancel, cancelText: '取消', success(res) { if (res.confirm) { // console.log('用戶點擊確定') wx.navigateTo({ url: '/pages/find/find' }) } else if (res.cancel) { // console.log('用戶點擊取消') } } }) }, 復制代碼
- 接口的請求需要一些時間,在切歌、請求各類數(shù)據(jù)、頁面加載時都有一段時間的等待期,為了提高用戶的友好性,在加載時最好加上一些等待動畫,我這里就直接使用的比較簡單的方法,在wxml中加上一個
loading
標簽,通過數(shù)據(jù)源中的hidden
,來控制loading
動畫是否顯示,一開始設置為false
,,然后在數(shù)據(jù)請求完成后,將其更改為true
。
wxml中:
<loading hidden="{{hidden}}">
拼命加載中...
</loading>
復制代碼
音頻播放
上面提到,在接口請求回音頻路徑之后,就會調用這個函數(shù),把請求會的數(shù)據(jù)作為參數(shù)傳過來,那現(xiàn)在就來剖析這個函數(shù)吧。
// 背景音頻播放方法
createBackgroundAudioManager(res) {
const that = this;//將this對象復制給that
const backgroundAudioManager = wx.getBackgroundAudioManager(); //調用官方API獲取全局唯一的背景音頻管理器。
console.log(backgroundAudioManager.src);
if (res.url != null) {
if (backgroundAudioManager.src != res.url) { //首次放歌或者切歌
that.setData({ //重設一下進度,避免切歌部分數(shù)據(jù)更新過慢
currentTime: '00:00', //當前音樂時間(00:00格式)
currentProcessNum: 0, //當前音樂時間(秒)
marginTop: 0, //文稿滾動距離
currentIndex: 0, //當前正在第幾行
})
backgroundAudioManager.title = that.data.song.name; //把title音頻標題給實例
backgroundAudioManager.singer = that.data.song.ar[0].name; //音頻歌手給實例
backgroundAudioManager.coverImgUrl = that.data.song.al.picUrl; //音頻圖片 給實例
backgroundAudioManager.src = http://www.wxapp-union.com/res.url; // 設置backgroundAudioManager的src屬性,音頻會立即播放
let musicId = that.data.musicId
app.globalData.history_songId = that.unique(app.globalData.history_songId, musicId) //去除重復歷史
}
that.setData({
isPlay: true, //是否播放設置為true
hidden: true, //隱藏加載動畫
backgroundAudioManager
})
}
app.globalData.backgroundAudioManager = backgroundAudioManager
//監(jiān)聽背景音樂進度更新事件
backgroundAudioManager.onTimeUpdate(() => {
that.setData({
totalProcessNum: backgroundAudioManager.duration,
currentTime: that.formatSecond(backgroundAudioManager.currentTime),
duration: that.formatSecond(backgroundAudioManager.duration)
})
if (!that.data.slide) { //如果進度條在滑動,就暫停更新進度條進度,否則會出現(xiàn)進度條進度來回閃動
that.setData({
currentProcessNum: backgroundAudioManager.currentTime,
})
}
if (!that.data.noLyric) { //如果沒有歌詞,就不需要調整歌詞位置
that.lyricsRolling(backgroundAudioManager)
}
})
backgroundAudioManager.onEnded(() => { //監(jiān)聽背景音樂自然結束事件,結束后自動播放下一首。自然結束,調用go_lastSong()函數(shù),即歌曲結束自動播放下一首歌
that.nextSong();
})
},
復制代碼
音頻播放函數(shù)里面的邏輯相對比較復雜,大致思路如下:
- 首先先創(chuàng)建一個
BackgroundAudioManager
實例,通過wx.getBackgroundAudioManager
獲取。 然后這里就需要做一個判斷,因為當調用本方法有幾種情況,一是首次放歌或切換歌曲、二是進來沒切換歌曲,所以要判斷當前音樂id獲取url地址是否等于backgroundAudioManager.src
,如果不相等,那就是第一種情況,需要將歌曲的musicId
調用unique()
去重方法,存入全局的history_songId[]
,這個歷史歌單主要用來給用戶切換上一首歌曲用的,后面會詳細講 然后給實例設置title
、singer
、coverImgURL
、src
、當設置了新的src
時,音樂會自動開始播放,設置這些屬性,主要用于原生音頻播放器的顯示以及分享,(注意title必須設置),設置之后,在手機上使用小程序播放音樂,就會出現(xiàn)一個原生音頻播放器,如圖:
歷史歌單去重
作用:用戶每播放一首歌,就將其存入歷史列表中,在存入之前,先判斷這首歌是否已經(jīng)存在,如果不存在,直接存入到歷史歌單數(shù)組后面,如果這首歌已經(jīng)存在,那就先去除老記錄,存入新紀錄。
// 歷史歌單去重
unique(arr, musicId) {
let index = arr.indexOf(musicId) //使用indexOf方法,判斷當前musicId是否已經(jīng)存在,如果存在,得到其下標
if (index != -1) { //如果已經(jīng)存在在歷史播放中,則刪除老記錄,存入新記錄
arr.splice(index, 1)
arr.push(musicId)
} else {
arr.push(musicId) //如果不存在,則直接存入歷史歌單
}
return arr //返回新的數(shù)組
},
復制代碼
- 第二步就是更新數(shù)據(jù)源的一些數(shù)據(jù),操作和作用都比較簡單,就不詳講了
- 第三步就很重要了,使用
backgroundAudioManager.onTimeUpdate()
監(jiān)聽背景音樂的進度更新,頁面進度條的秒數(shù)更新就和這有關! wxml:
<view class="page-slider">
<view>
{{currentTime}}
</view>
<slider class="slider_middle" bindchange="end" bindtouchstart="start" max="{{totalProcessNum}}" min="0" backgroundColor="rgba(255,255,255,.3)"
activeColor="rgba(255,255,255,.8)" value="http://www.wxapp-union.com/{{currentProcessNum}}" block-size="12"></slider>
<view>
{{duration}}
</view>
</view>
復制代碼
backgroundAudioManager.currentTime
和backgroundAudioManager.currentTime
分別會返回音頻播放位置和音頻長度,單位為秒,而進度條左邊的當前時間和右邊的歌曲總時長需要顯示成00:00的格式,所以使用formatSecond()
來格式化秒數(shù)
格式化時間
// 格式化時間
formatSecond(second) {
var secondType = typeof second;
if (secondType === "number" || secondType === "string") {
second = parseInt(second);
var minute = Math.floor(second / 60);
second = second - minute * 60;
return ("0" + minute).slice(-2) + ":" + ("0" + second).slice(-2);
} else {
return "00:00";
}
},
復制代碼
歌詞滾動
<!-- 歌詞 -->
<!-- 需要設置高度,否則scroll-top可能失效 -->
<scroll-view
hidden="{{hiddenLyric}}"
scroll-y="true"
scroll-with-animation='true'
scroll-top='{{marginTop}}'
class="body-scroll"
>
<view class='contentText'>
<view class="contentText-noLyric" wx:if="{{noLyric==true}}">純音樂,無歌詞 </view>
<block wx:for='{{storyContent}}' wx:key="index">
<view class="lyric">
<view class="lyric-text {{currentIndex == index ? 'currentTime' : ''}}">{{item[1]}}</view>
</view>
</block>
</view>
</scroll-view>
復制代碼
- 歌詞的隨屏滾動通過歌詞時間和音頻當前位置來判斷當前歌詞是多少行,自動滾動是用行數(shù)來計算高度,通過設置數(shù)據(jù)源的
marginTop
,這個值作用于scroll-view
的scroll-top
,實現(xiàn)自動滾動的,需要注意的是,scroll-view
需要設置高度,否則scroll-top
可能失效 - 通過判斷
currentIndex
是否和頁面for循環(huán)中的index值是否相等,來給當前唱的歌詞加上類名,使其高亮顯示。
// 歌詞滾動方法
lyricsRolling(backgroundAudioManager) {
const that = this
// 歌詞滾動
that.setData({
marginTop: (that.data.currentIndex - 3) * 39
})
// 當前歌詞對應行顏色改變
if (that.data.currentIndex != that.data.storyContent.length - 1) {//不是最后一行
// var j = 0;
for (let j = that.data.currentIndex; j < that.data.storyContent.length; j++) {
// 當前時間與前一行,后一行時間作比較, j:代表當前行數(shù)
if (that.data.currentIndex == that.data.storyContent.length - 2) { //倒數(shù)第二行
//最后一行只能與前一行時間比較
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[that.data.storyContent.length - 1][0])) {
that.setData({
currentIndex: that.data.storyContent.length - 1
})
return;
}
} else {
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[j][0]) && parseFloat(backgroundAudioManager.currentTime) < parseFloat(that.data.storyContent[j + 1][0])) {
that.setData({
currentIndex: j
})
return;
}
}
}
}
},
復制代碼
進度條事件
在進度條開始滑動的時候將數(shù)據(jù)源中的slide
設置為true,這時backgroundAudioManager.onTimeUpdate()
中的更新數(shù)據(jù)源currentProcessNum
就不會再進行,這樣就緩解了進度條抖動的問題。
抖動問題:如圖,在拖動進度條想快進或者快退音樂的時候,可以看到小滑塊非常明顯的抖動,這是由于onTimeUpdate()
在不停的監(jiān)聽并更改數(shù)據(jù)源中的currentProcessNum
,導致拖動過程中的小滑塊不停的前后跳動。
//進度條開始滑動觸發(fā)
start: function (e) {
// 控制進度條停,防止出現(xiàn)進度條抖動
this.setData({
slide: true
})
},
復制代碼
結束滑動的時候,通過backgroundAudioManager.seek(position)
來讓音頻跳到指定位置,然后判斷當前歌詞到了多少行,立馬設置數(shù)據(jù)源中的currentIndex
,讓歌詞就會在上面的歌詞跳轉方法中改變marginTop
的值,歌詞就會跳轉到相應的位置。
//結束滑動觸發(fā)
end: function (e) {
const position = e.detail.value
let backgroundAudioManager = this.data.backgroundAudioManager //獲取背景音頻實例
// console.log(position)
backgroundAudioManager.seek(position) //改變歌曲進度
this.setData({
currentProcessNum: position,
slide: false
})
// 判斷當前是多少行
for (let j = 0; j < this.data.storyContent.length; j++) {
// console.log('當前行數(shù)', this.data.currentIndex)
// console.log(parseFloat(backgroundAudioManager.currentTime))
// console.log(parseFloat(this.data.storyContent[j][0]))
// 當前時間與前一行,后一行時間作比較, j:代表當前行數(shù)
if (position < parseFloat(this.data.storyContent[j][0])) {
this.setData({
currentIndex: j - 1
})
return;
}
}
}
復制代碼
- 第四步使用
backgroundAudioManager.onEnded()
監(jiān)聽背景音樂的自然結束,結束就調用nextSong()
函數(shù),這個函數(shù)用來播放待放列表里面的歌。
播放上一首、播放下一首
push()
到歷史列表,那么將當前歌曲(把歷史列表數(shù)組里面的最后一項從數(shù)組刪除,并將其頭插加入到待播放列表)放入待放歌單,然后調用play()
方法就好了(傳入刪除了最后一項之后新的歷史列表數(shù)組的最后一項,即原歷史列表的倒數(shù)第二項)
// 播放上一首歌曲
beforeSong() {
if (app.globalData.history_songId.length > 1) { //前面有歌
app.globalData.waitForPlaying.unshift(app.globalData.history_songId.pop())//將當前播放歌曲從前插入待放列表
this.play(app.globalData.history_songId[app.globalData.history_songId.length - 1]) //播放歷史歌單歌曲
} else {
this.tips('前面沒有歌曲了哦', '去選歌', true)
}
},
復制代碼
播放下一首歌曲,如果待播放列表數(shù)組長度大于0,那就把數(shù)組第一個元素刪除并返回傳入到play()
方法中
// 下一首歌曲
nextSong() {
if (app.globalData.waitForPlaying.length > 0) {
this.play(app.globalData.waitForPlaying.shift())//刪除待放列表第一個元素并返回播放
} else {
this.tips('后面沒有歌曲了哦', '去選歌', true)
}
},
復制代碼
暫停和播放
比較簡單,拿到數(shù)據(jù)原中的backgroundAudioManager
,通過其自帶的pause()
、play()
的方法就可以實現(xiàn)播放和暫停
// 播放和暫停
handleToggleBGAudio() {
const backgroundAudioManager = this.data.backgroundAudioManager
//如果當前在播放的話
if (this.data.isPlay) {
backgroundAudioManager.pause();//暫停
} else { //如果當前處于暫停狀態(tài)
backgroundAudioManager.play();//播放
}
this.setData({
isPlay: !this.data.isPlay
})
},
復制代碼
所有代碼
-
wxml
<view class="body-view"> <loading hidden="{{hidden}}"> 拼命加載中... </loading> </view> <!-- 音樂播放背景圖片 --> <image class="background_img" src="http://www.wxapp-union.com/{{song.al.picUrl}}"></image> <view class="play-wrapper"> <view class="play-title"> <view class="title-back" bindtap="backPage"> <image class="" src="http://www.wxapp-union.com/image/play_back.png" /> </view> <view class="singer-info"> <view class="title-songName">{{song.name}}</view> <view class="title-singer"> <text wx:for="{{song.ar}}" wx:key="index" class="singer-name-text">{{item.name}}</text> </view> </view> </view> <view class="play-body" bindtap="showLyric"> <!-- 歌詞 --> <!-- 需要設置高度,否則scroll-top可能失效 --> <scroll-view hidden="{{hiddenLyric}}" scroll-y="true" scroll-with-animation='true' scroll-top='{{marginTop}}' class="body-scroll" > <view class='contentText'> <view class="contentText-noLyric" wx:if="{{noLyric==true}}">純音樂,無歌詞 </view> <block wx:for='{{storyContent}}' wx:key="index"> <view class="lyric"> <view class="lyric-text {{currentIndex == index ? 'currentTime' : ''}}">{{item[1]}}</view> </view> </block> </view> </scroll-view> <!-- 添加轉動動畫 --> <view hidden="{{!hiddenLyric}}"> <image class="body-record" style="{{isPlay?'':'transform:rotate(-40deg)'}}" mode="aspectFit" src="http://www.wxapp-union.com/image/play_stick.png" ></image> <view class="body-round run" style="{{isPlay?'animation-play-state:running;':'animation-play-state:paused;'}}"> <view class="round-container"> <image class="round-img" src="https://s3.music.126.net/mobile-new/img/disc-ip6.png?69796123ad7cfe95781ea38aac8f2d48="></image> <image class="singer-img" src="http://www.wxapp-union.com/{{song.al.picUrl}}"></image> </view> </view> </view> </view> <view> </view> <!-- 一開始onload時,hiddenLyric=true, 顯示為轉動的圖標,點擊圖標,切換為歌詞--> <view class="sing-show"> <view class="moveCircle {{isPlay ? 'play' : ''}}" hidden="{{!hiddenLyric}}"> </view> </view> <!-- 底部播放暫停圖標 --> <view class="play-foot"> <view class="page-slider"> <view> {{currentTime}} </view> <slider class="slider_middle" bindchange="end" bindtouchstart="start" max="{{totalProcessNum}}" min="0" backgroundColor="rgba(255,255,255,.3)" activeColor="rgba(255,255,255,.8)" value="http://www.wxapp-union.com/{{currentProcessNum}}" block-size="12" ></slider> <view> {{duration}} </view> </view> <!-- 上一首歌 --> <view class="play_suspend"> <view class="icon_playing "> <image src="http://www.wxapp-union.com/image/play_lastSong.png" class=" icon_play" bindtap="beforeSong" /> </view> <!-- 暫停圖標--> <view class="icon_playing"> <image bindtap="handleToggleBGAudio" src="http://www.wxapp-union.com/image/play_suspend.png" hidden="{{!isPlay}}" class="img_play" /> <!--播放圖標--> <image bindtap="handleToggleBGAudio" src="http://www.wxapp-union.com/image/play_play.png" hidden="{{isPlay}}" class="img_play" /> </view> <!-- 下一首歌 --> <view class="icon_playing "> <image src="http://www.wxapp-union.com/image/play_nextSong.png" class="icon_play" bindtap="nextSong" /> </view> </view> </view> </view> 復制代碼
-
css
.background_img {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
/* css3濾鏡屬性,blur設置高斯模糊 brightness設置變暗*/
filter: blur(30px) brightness(50%);
z-index: -1;
transform: scale(1.5);
}
.play-wrapper {
width: 100wh;
height: 100vh;
}
.play-title {
padding: 90rpx 30rpx;
display: flex;
align-items: center;
}
/* 返回箭頭 */
.title-back {
width: 50rpx;
height: 50rpx;
}
.title-back image {
width: 100%;
height: 100%;
}
.play-body {
width: 750rpx;
height: 814rpx;
overflow: scroll;
}
.body-scroll {
height: 100%;
}
/* 歌手信息 */
.singer-info {
color: #ffffff;
margin-left: 40rpx;
font-size: 36rpx;
}
/* 歌名 */
.title-songName {
max-width: 400rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 歌手名 */
.title-singer {
font-size: 24rpx;
margin-top: 10rpx;
color: rgba(255, 255, 255, 0.3)
}
.body-record {
position: absolute;
left: 42%;
top: 18%;
width: 250rpx;
height: 250rpx;
z-index: 100;
background-size: contain;
/* 設置旋轉基點 */
transform-origin: 4vw 4vw;
/* 控制任何一個樣式的變化,添加一個延遲的時間效果 ,在這里監(jiān)聽transform的變化*/
transition: transform 1s ease 0s;
}
/* 定義轉動動畫 */
.turn {
animation: turn 5s linear;
}
/*
rturnun : 定義的動畫名稱
5s : 動畫時間
linear : 動畫以何種運行軌跡完成一個周期
infinite :規(guī)定動畫應該無限次播放
*/
@keyframes turn {
0% {
-webkit-transform: rotate(0deg);
}
25% {
-webkit-transform: rotate(-90deg);
}
}
.run {
animation: run 25s linear infinite;
}
/*
run : 定義的動畫名稱
1s : 動畫時間
linear : 動畫以何種運行軌跡完成一個周期
infinite :規(guī)定動畫應該無限次播放
*/
@keyframes run {
0% {
-webkit-transform: rotate(0deg);
}
25% {
-webkit-transform: rotate(90deg);
}
50% {
-webkit-transform: rotate(180deg);
}
75% {
-webkit-transform: rotate(270deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
.body-round {
display: flex;
justify-content: center;
align-items: center;
width: 100wh;
height: 814rpx;
}
.round-container {
width: 630rpx;
height: 630rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.round-img {
position: absolute;
width: 630rpx;
height: 630rpx;
}
.singer-img {
width: 580rpx;
height: 580rpx;
border-radius: 50%;
}
.play-foot {
position: fixed;
width: 750rpx;
height: 250rpx;
bottom: 0;
}
/* 音樂進度條 */
.page-slider {
display: flex;
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
justify-content: space-evenly;
align-items: center;
}
.slider_middle {
width: 65%;
}
.play_suspend {
display: flex;
justify-content: space-around;
align-items: center;
}
.icon_play {
width: 60rpx;
height: 60rpx;
}
.img_play {
width: 100rpx;
height: 100rpx;
}
.currentTime {
color: rgba(255, 255, 255, 1);
}
.lyric {
display: flex;
justify-content: center;
color: rgba(255, 255, 255, 0.6);
padding: 20rpx 40rpx;
font-size: 28rpx;
}
.lyric-text {
text-align: center;
}
.contentText-noLyric {
text-align: center;
margin-top: 200rpx;
color: rgba(255, 255, 255, 1);
}
復制代碼
- js
const $api = require('../../utils/api.js').API;
const app = getApp();
Page({
/**
* 頁面的初始數(shù)據(jù)
*/
data: {
musicId: -1,//音樂id
hidden: false, //加載動畫是否隱藏
isPlay: true, //歌曲是否播放
song: [], //歌曲信息
hiddenLyric: true, //是否隱藏歌詞
backgroundAudioManager: {}, //背景音頻對象
duration: '', //總音樂時間(00:00格式)
currentTime: '00:00', //當前音樂時間(00:00格式)
totalProcessNum: 0, //總音樂時間 (秒)
currentProcessNum: 0, //當前音樂時間(秒)
storyContent: [], //歌詞文稿數(shù)組,轉化完成用來在頁面中使用
marginTop: 0, //文稿滾動距離
currentIndex: 0, //當前正在第幾行
noLyric: false, //是否有歌詞
slide: false //進度條是否在滑動
},
//返回上一頁
backPage: function () {
wx: wx.navigateBack({
delta: 1
});
},
/**
* 生命周期函數(shù)--監(jiān)聽頁面加載
*/
onLoad: function (options) {
const musicId = options.musicId //獲取到其他頁面?zhèn)鱽淼膍usicId
this.play(musicId) //調用play方法
},
//播放音樂方法
play(musicId) {
const that = this;//將this對象復制給that
that.setData({
hidden: false,
musicId
})
app.globalData.musicId = musicId // 將當前音樂id傳到全局
// 通過musicId發(fā)起接口請求,請求歌曲詳細信息
//獲取到歌曲音頻,則顯示出歌曲的名字,歌手的信息,即獲取歌曲詳情;如果失敗,則播放出錯。
$api.getSongDetail({ ids: musicId }).then(res => {
if (res.data.songs.length === 0) {
that.tips('服務器正忙~~', '確定', false)
} else { //獲取成功
app.globalData.songName = res.data.songs[0].name
that.setData({
song: res.data.songs[0], //獲取到歌曲的詳細內容,傳給song
})
wx.request({ // 獲取歌詞
url: 'http://47.98.159.95/m-api/lyric',
data: {
id: musicId
},
success: res => {
if (res.data.nolyric || res.data.uncollected) { //該歌無歌詞,或者歌詞未收集
that.setData({
noLyric: true
})
}
else { //有歌詞
that.setData({
storyContent: that.sliceNull(that.parseLyric(res.data.lrc.lyric))
})
}
}
})
// 通過音樂id獲取音樂的地址,請求歌曲音頻的地址,失敗則播放出錯,成功則傳值給createBackgroundAudioManager(后臺播放管理器,讓其后臺播放)
$api.getSongUrl({ id: musicId }).then(res => {
//請求成功
if (res.data.data[0].url === null) { //獲取出現(xiàn)錯誤出錯
that.tips('音樂播放出了點狀況~~', '確定', false)
} else {
// 調用createBackgroundAudioManager方法將歌曲url傳入backgroundAudioManager
that.createBackgroundAudioManager(res.data.data[0]);
}
})
.catch(err => {
//請求失敗
that.tips('服務器正忙~~', '確定', false)
})
}
})
.catch(err => {
//請求失敗
that.tips('服務器正忙~~', '確定', false)
})
},
// 背景音頻播放方法
createBackgroundAudioManager(res) {
const that = this;//將this對象復制給that
const backgroundAudioManager = wx.getBackgroundAudioManager(); //調用官方API獲取全局唯一的背景音頻管理器。
if (res.url != null) {
if (backgroundAudioManager.src != res.url) { //首次放歌或者切歌
that.setData({ //重設一下進度,避免切歌部分數(shù)據(jù)更新過慢
currentTime: '00:00', //當前音樂時間(00:00格式)
currentProcessNum: 0, //當前音樂時間(秒)
marginTop: 0, //文稿滾動距離
currentIndex: 0, //當前正在第幾行
})
backgroundAudioManager.title = that.data.song.name; //把title音頻標題給實例
backgroundAudioManager.singer = that.data.song.ar[0].name; //音頻歌手給實例
backgroundAudioManager.coverImgUrl = that.data.song.al.picUrl; //音頻圖片 給實例
backgroundAudioManager.src = http://www.wxapp-union.com/res.url; // 設置backgroundAudioManager的src屬性,音頻會立即播放
let musicId = that.data.musicId
app.globalData.history_songId = that.unique(app.globalData.history_songId, musicId) //去除重復歷史
}
that.setData({
isPlay: true, //是否播放設置為true
hidden: true, //隱藏加載動畫
backgroundAudioManager
})
}
app.globalData.backgroundAudioManager = backgroundAudioManager
//監(jiān)聽背景音樂進度更新事件
backgroundAudioManager.onTimeUpdate(() => {
that.setData({
totalProcessNum: backgroundAudioManager.duration,
currentTime: that.formatSecond(backgroundAudioManager.currentTime),
duration: that.formatSecond(backgroundAudioManager.duration)
})
if (!that.data.slide) { //如果進度條在滑動,就暫停更新進度條進度,否則會出現(xiàn)進度條進度來回閃動
that.setData({
currentProcessNum: backgroundAudioManager.currentTime,
})
}
if (!that.data.noLyric) { //如果沒有歌詞,就不需要調整歌詞位置
that.lyricsRolling(backgroundAudioManager)
}
})
backgroundAudioManager.onEnded(() => { //監(jiān)聽背景音樂自然結束事件,結束后自動播放下一首。自然結束,調用go_lastSong()函數(shù),即歌曲結束自動播放下一首歌
that.nextSong();
})
// console.log('待放', app.globalData.waitForPlaying)
// console.log('歷史', app.globalData.history_songId)
},
// 提醒
tips(content, confirmText, isShowCancel) {
wx.showModal({
content: content,
confirmText: confirmText,
cancelColor: '#DE655C',
confirmColor: '#DE655C',
showCancel: isShowCancel,
cancelText: '取消',
success(res) {
if (res.confirm) {
// console.log('用戶點擊確定')
wx.navigateTo({
url: '/pages/find/find'
})
} else if (res.cancel) {
// console.log('用戶點擊取消')
}
}
})
},
// 歷史歌單去重
unique(arr, musicId) {
let index = arr.indexOf(musicId) //使用indexOf方法,判斷當前musicId是否已經(jīng)存在,如果存在,得到其下標
if (index != -1) { //如果已經(jīng)存在在歷史播放中,則刪除老記錄,存入新記錄
arr.splice(index, 1)
arr.push(musicId)
} else {
arr.push(musicId) //如果不存在,則直接存入歷史歌單
}
return arr //返回新的數(shù)組
},
// 歌詞滾動方法
lyricsRolling(backgroundAudioManager) {
const that = this
// 歌詞滾動
that.setData({
marginTop: (that.data.currentIndex - 3) * 39
})
// 當前歌詞對應行顏色改變
if (that.data.currentIndex != that.data.storyContent.length - 1) {//不是最后一行
// var j = 0;
for (let j = that.data.currentIndex; j < that.data.storyContent.length; j++) {
// 當前時間與前一行,后一行時間作比較, j:代表當前行數(shù)
if (that.data.currentIndex == that.data.storyContent.length - 2) { //倒數(shù)第二行
//最后一行只能與前一行時間比較
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[that.data.storyContent.length - 1][0])) {
that.setData({
currentIndex: that.data.storyContent.length - 1
})
return;
}
} else {
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[j][0]) && parseFloat(backgroundAudioManager.currentTime) < parseFloat(that.data.storyContent[j + 1][0])) {
that.setData({
currentIndex: j
})
return;
}
}
}
}
},
// 格式化時間
formatSecond(second) {
var secondType = typeof second;
if (secondType === "number" || secondType === "string") {
second = parseInt(second);
var minute = Math.floor(second / 60);
second = second - minute * 60;
return ("0" + minute).slice(-2) + ":" + ("0" + second).slice(-2);
} else {
return "00:00";
}
},
// 播放上一首歌曲
beforeSong() {
if (app.globalData.history_songId.length > 1) {
app.globalData.waitForPlaying.unshift(app.globalData.history_songId.pop())//將當前播放歌曲從前插入待放列表
this.play(app.globalData.history_songId[app.globalData.history_songId.length - 1]) //播放歷史歌單歌曲
} else {
this.tips('前面沒有歌曲了哦', '去選歌', true)
}
},
// 下一首歌曲
nextSong() {
if (app.globalData.waitForPlaying.length > 0) {
this.play(app.globalData.waitForPlaying.shift())//刪除待放列表第一個元素并返回播放
} else {
this.tips('后面沒有歌曲了哦', '去選歌', true)
}
},
// 播放和暫停
handleToggleBGAudio() {
const backgroundAudioManager = this.data.backgroundAudioManager
//如果當前在播放的話
if (this.data.isPlay) {
backgroundAudioManager.pause();//暫停
} else { //如果當前處于暫停狀態(tài)
backgroundAudioManager.play();//播放
}
this.setData({
isPlay: !this.data.isPlay
})
},
// 點擊切換歌詞和封面
showLyric() {
this.setData({
hiddenLyric: !this.data.hiddenLyric
})
},
//去除空白行
sliceNull: function (lrc) {
var result = []
for (var i = 0; i < lrc.length; i++) {
if (lrc[i][1] !== "") {
result.push(lrc[i]);
}
}
return result
},
//格式化歌詞
parseLyric: function (text) {
let result = [];
let lines = text.split('\n'), //切割每一行
pattern = /\[\d{2}:\d{2}.\d+\]/g;//用于匹配時間的正則表達式,匹配的結果類似[xx:xx.xx]
// console.log(lines);
//去掉不含時間的行
while (!pattern.test(lines[0])) {
lines = lines.slice(1);
};
//上面用'\n'生成數(shù)組時,結果中最后一個為空元素,這里將去掉
lines[lines.length - 1].length === 0 && lines.pop();
lines.forEach(function (v /*數(shù)組元素值*/, i /*元素索引*/, a /*數(shù)組本身*/) {
//提取出時間[xx:xx.xx]
var time = v.match(pattern),
//提取歌詞
value = http://www.wxapp-union.com/v.replace(pattern,'');
// 因為一行里面可能有多個時間,所以time有可能是[xx:xx.xx][xx:xx.xx][xx:xx.xx]的形式,需要進一步分隔
time.forEach(function (v1, i1, a1) {
//去掉時間里的中括號得到xx:xx.xx
var t = v1.slice(1, -1).split(':');
//將結果壓入最終數(shù)組
result.push([parseInt(t[0], 10) * 60 + parseFloat(t[1]), value]);
});
});
// 最后將結果數(shù)組中的元素按時間大小排序,以便保存之后正常顯示歌詞
result.sort(function (a, b) {
return a[0] - b[0];
});
return result;
},
//進度條開始滑動觸發(fā)
start: function (e) {
// 控制進度條停,防止出現(xiàn)進度條抖動
this.setData({
slide: true
})
},
//結束滑動觸發(fā)
end: function (e) {
const position = e.detail.value //移動值
let backgroundAudioManager = this.data.backgroundAudioManager //獲取背景音頻實例
this.setData({
currentProcessNum: position,
slide: false
})
backgroundAudioManager.seek(position) //改變歌曲進度
// 判斷當前是多少行
for (let j = 0; j < this.data.storyContent.length; j++) {
// 當前時間與前一行,后一行時間作比較, j:代表當前行數(shù)
if (position < parseFloat(this.data.storyContent[j][0])) {
this.setData({
currentIndex: j - 1
})
return;
}
}
}
})
復制代碼
總結
本項目并不復雜,適合初學者上手,因為免去了寫復雜的后端,只用寫好js邏輯就可以,并且在聽到自己仿的小程序可以放出音樂的時候會有很大的成就感,但是同時還是存在一些小坑等待大家處理的,在寫本小程序的時候,我也是遇到了挺多問題的,遇到問題先思考,想不出來,就去看看別的大佬寫的經(jīng)驗分享,由于本人經(jīng)驗不是特別豐富,只是淺淺入門,很多問題的解決思考的并不到位,如果個位發(fā)現(xiàn)我在代碼中有什么bug,歡迎個位讀者大大指出,期待我們的共同成長。
本項目只是用來練習鞏固知識使用,絕不作為商用,如有侵權,請聯(lián)系我做整改
作者:盛良
來源:掘金
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
相關案例查看更多
相關閱讀
- 小程序開發(fā)
- 云南軟件公司
- asp網(wǎng)站
- 做網(wǎng)站
- 昆明做網(wǎng)站
- 網(wǎng)站建設快速優(yōu)化
- 網(wǎng)站建設選
- 云南網(wǎng)絡推廣
- 旅游網(wǎng)站建設
- 云南網(wǎng)站建設首選
- 云南花農小程序
- 云南軟件設計
- 正規(guī)網(wǎng)站建設公司
- 云南網(wǎng)站建設公司地址
- 海南小程序制作公司
- 網(wǎng)站小程序
- 汽車報廢回收管理軟件
- 小程序模板開發(fā)公司
- 云南小程序開發(fā)推薦
- 云南小程序開發(fā)哪家好
- 昆明網(wǎng)站開發(fā)
- flex
- 云南小程序代建
- 網(wǎng)站開發(fā)哪家好
- 報廢車管理
- 分銷系統(tǒng)
- 云南微信小程序開發(fā)
- 小程序密鑰
- web前端
- 百度人工排名