項(xiàng)目部分截圖(Gif)
前言
前一陣子學(xué)習(xí)了微信小程序,為了鞏固所學(xué)的知識(shí)和提高實(shí)戰(zhàn)經(jīng)驗(yàn),決定自己手?jǐn)]一款小程序。因?yàn)槁?tīng)歌一直在用網(wǎng)易云音樂(lè),所以突發(fā)奇想就做一款仿網(wǎng)易云音樂(lè)的小程序吧!開(kāi)發(fā)中遇到了很多在學(xué)習(xí)中沒(méi)有遇到過(guò)的坑,也很感謝在我改不出BUG時(shí)給予幫助的老師同學(xué)!本著學(xué)習(xí)和分享的目的,寫(xiě)下以下的文字,希望能給初學(xué)小程序的你帶來(lái)一點(diǎn)幫助,大佬輕點(diǎn)噴。
開(kāi)發(fā)前準(zhǔn)備
- VScode代碼編輯器。
- 微信開(kāi)發(fā)者工具
- ios網(wǎng)易云音樂(lè)(V5.9.1版本)
- 酷狗音樂(lè)小程序(提供了一些思路)
- 網(wǎng)易云音樂(lè)API
- (阿里巴巴矢量圖標(biāo)庫(kù))提供一些圖標(biāo)icon
tabBar部分
自定義tabBar
一般在開(kāi)發(fā)中,微信小程序給我們的tabBar就能滿(mǎn)足需求。但是,有些特別的需求必須使用自定義tabBar才能滿(mǎn)足。
比如tabBar實(shí)現(xiàn)半透明。那么,如何才能自定義tabBar呢?
1.首先,在 app.json里的"tabBar"里聲明
"tabBar": { "custom": true }
2.接著在項(xiàng)目的根目錄下新建一個(gè)custom-tab-bar文件夾。里面包含index.wxml index.js index.json index.wxss四個(gè)文件。更多細(xì)節(jié)參考微信小程序文檔
<!-- index.html -->
<!-- 自定義tabbar頁(yè)面 -->
<cover-view class="tab-bar">
<cover-view class="tab-bar-border"></cover-view><!--tabBal邊框樣式 -->
<!-- 樂(lè)庫(kù)tabbar -->
<cover-view class='tab-bar-item' >
<cover-image src='../images/music.png' hidden='{{isShow_index}}' bindtap='switchTab_index'></cover-image>
<cover-image src='../images/selected-music.png' hidden='{{!isShow_index}}' bindtap='switchTab_index'></cover-image>
<cover-view style="color:{{isShow_index ? selectedColor : color}}">樂(lè)庫(kù)</cover-view>
</cover-view>
<!-- 播放tabbar -->
<cover-view class='tab-bar-item' bindtap='switchTab_playing'>
<cover-image src='../images/selected-playing.png' hidden='{{isShow_playing}}'></cover-image>
<cover-image src='../images/playing.png' hidden='{{!isShow_playing}}'></cover-image>
<cover-view></cover-view>
</cover-view>
<!-- 我的tabbar -->
<cover-view class='tab-bar-item' bindtap='switchTab_me'>
<cover-image src='../images/me.png' hidden='{{isShow_me}}'></cover-image>
<cover-image src='../images/selected-me.png' hidden='{{!isShow_me}}'></cover-image>
<cover-view style="color:{{isShow_me ? selectedColor : color}}">我的</cover-view>
</cover-view>
</cover-view>
// index.js
Component({
data: {
isShow_index:true,
isShow_playing:false,
isShow_me:false,
selected: 0, //首頁(yè)
color: "#8D8D8D",
selectedColor: "#C62F2F",
list: [{
pagePath: "/pages/index/index",
iconPath: "/images/music.png",
selectedIconPath: "/images/selected-music.png",
text: "樂(lè)庫(kù)"
}, {
pagePath: "/pages/love/love",
iconPath: "/images/selected-playing.png",
selectedIconPath: "/images/playing.png",
text: ""
},
{
pagePath: "/pages/me/me",
iconPath: "/images/me.png",
selectedIconPath: "/images/selected-me.png",
text: "我的"
}]
},
methods: {
switchTab_index:function(){
wx.switchTab({
url:'/pages/index/index'
})
this.setData({
isShow_index: true,
isShow_me: false,
isShow_playing: false
})
},
switchTab_playing: function () {
wx.switchTab({
url: '/pages/love/love'
})
this.setData({
isShow_playing: true,
isShow_index: false,
isShow_me: false
})
},
switchTab_me: function () {
wx.switchTab({
url: '/pages/me/me'
})
this.setData({
isShow_me:true,
isShow_playing: false,
isShow_index: false
})
}
}
})
tabBar半透明
/* custom-tab-bar/index.wxss */
.tab-bar {
height:7%;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background:#FAFBFD;
opacity: 0.93;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}
API封裝
一般我們https請(qǐng)求都是通過(guò)wx.request來(lái)請(qǐng)求,但是這種方法只能請(qǐng)求一次數(shù)據(jù),如果首頁(yè)用wx.request來(lái)請(qǐng)求的話(huà),代碼看起來(lái)會(huì)很冗長(zhǎng)和雜亂。不僅自己容易搞糊涂,其他人看代碼時(shí)也會(huì)很累。因此為了代碼的整潔干凈,我在這里新建了一個(gè)文件專(zhuān)門(mén)存放API。一般在根目錄下的utils文件夾下新建一個(gè)api.js,但我在根目錄下新建了文件夾API,里面包含api.js。
// api.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const request = (url, data) => {
let _url = API_BASE_URL + url;
return new Promise((resolve, reject) => {
wx.request({
url: _url,
method: "get",
data: data,
header: {
'Content-Type': 'application/x-www-form-urlencoded'
},
success(request) {
resolve(request.data)
},
fail(error) {
reject(error)
}
})
});
}
module.exports ={
gethotsongs:(data) =>{
return request('/search/hot',data)//熱搜接口
},
searchSuggest:(data)=>{
return request('/search/suggest',data)//搜索建議接口
},
searchResult:(data)=>{
return request('/search',data)//搜索結(jié)果接口
},
getBanner:(data)=>{
return request('/banner',data)//個(gè)性推薦輪播
},
getsongsheet:(data)=>{
return request('/top/playlist',data)//熱門(mén)歌單接口
},
getNewSong:(data)=>{
return request('/personalized/newsong',data)//最新音樂(lè)接口
},
getDjRadios:(data)=>{
return request('/dj/recommend',data)//電臺(tái)推薦接口
},
getProgramRecommend:(data)=>{
return request('/program/recommend',data)//推薦節(jié)目接口
},
getRecommendType:(data)=>{
return request('/dj/recommend/type',data)//所有電臺(tái)分類(lèi)推薦
},
getRecommendMV:(data)=>{
return request('/personalized/mv',data)//推薦MV
},
getNewMv:(data)=>{
return request('/mv/first',data)//最新MV
},
getNewEst:(data)=>{
return request('/album/newest',data)//最新專(zhuān)輯
},
getTopList:(data)=>{
return request('/top/list',data)//排行榜
},
getDjList:(data)=>{
return request('/dj/catelist',data) //電臺(tái)分類(lèi)
},
getPay:(data)=>{
return request('/dj/paygift',data)//付費(fèi)精品
},
getSonger:(data)=>{
return request('/toplist/artist',data)//歌手排行
}
}
api.js只能通過(guò)module.exports來(lái)暴露,那個(gè)頁(yè)面要數(shù)據(jù)就從這拿。如果在哪個(gè)頁(yè)面要用到它,還需要在頭部引入一下:
const API = require('../../API/api')
以個(gè)性推薦輪播圖為例,
getBanner: function() {
API.getBanner({
type: 2
}).then(res => {
if (res.code === 200) { //更加嚴(yán)謹(jǐn)
this.setData({
banner: res.banners
})
}
})
}
這樣就把請(qǐng)求到的數(shù)據(jù)存儲(chǔ)到banner中了。
搜索部分
輸入框樣式
我這里是引入了
WEUI的樣式,1.下載
weui.wxss,鏈接我找不到了,所以我放上了我的github上的weui.wxss。2.把下載好的
weui.wxss放到根目錄下。3.在
app.wxss中@import "weui.wxss";引入一下就可以使用微信提供給我們的樣式了。4.
WeUI樣式庫(kù)
熱門(mén)搜索
上面已經(jīng)提到我從
api.js中拿數(shù)據(jù)。
// 從接口到獲取到數(shù)據(jù)導(dǎo)入到hotsongs
gethotsongs() {
API.gethotsongs({ type: 'new' }).then(res => {
wx.hideLoading()
if (res.code === 200) { //嚴(yán)謹(jǐn)
this.setData({
hotsongs: res.result.hots
})
}
})
}
搜索歷史
思路:當(dāng)在輸入框輸入完成后-->失去焦點(diǎn)--> 利用
wx.setStorageSync存進(jìn)緩存中-->wx.getStorageSync獲取到并把它打印出來(lái)。
// input失去焦點(diǎn)函數(shù)
routeSearchResPage: function(e) {
console.log(e.detail.value)
let history = wx.getStorageSync("history") || [];
history.push(this.data.searchKey)
wx.setStorageSync("history", history);
},
//每次顯示變動(dòng)就去獲取緩存,給history,并for出來(lái)。
onShow: function () {
this.setData({
history: wx.getStorageSync("history") || []
})
},
清空搜索歷史
思路:×圖標(biāo)綁定事件->呼出對(duì)話(huà)框
wx.showModal->確定則把history賦值為空
// 清空page對(duì)象data的history數(shù)組 重置緩存為[]
clearHistory: function() {
const that = this;
wx.showModal({
content: '確認(rèn)清空全部歷史記錄',
cancelColor:'#DE655C',
confirmColor: '#DE655C',
success(res) {
if (res.confirm) {
that.setData({
history: []
})
wx.setStorageSync("history", []) //把空數(shù)組給history,即清空歷史記錄
} else if (res.cancel) {
}
}
})
},
實(shí)時(shí)搜索建議
思路:實(shí)時(shí)獲取輸入框的值->把值傳給搜索建議API,發(fā)起網(wǎng)絡(luò)請(qǐng)求->請(qǐng)求之后拿到搜索建議->打印結(jié)果并隱藏其他組件只保留搜索建議的組件(類(lèi)似于Vue里的v-show)
//獲取input文本并且實(shí)時(shí)搜索,動(dòng)態(tài)隱藏組件
getsearchKey:function(e){
console.log(e.detail.value) //打印出輸入框的值
let that = this;
if(e.detail.cursor != that.data.cursor){ //實(shí)時(shí)獲取輸入框的值
that.setData({
searchKey: e.detail.value
})
}
if(e.value!=""){ //組件的顯示與隱藏
that.setData({
showView: false
})
} else{
that.setData({
showView: ""
})
}
if(e.detail.value!=""){ //解決 如果輸入框的值為空時(shí),傳值給搜索建議,會(huì)報(bào)錯(cuò)的bug
that.searchSuggest();
}
}
// 搜索建議
searchSuggest(){
API.searchSuggest({ keywords: this.data.searchKey ,type:'mobile'}).then(res=>{
if(res.code === 200){
this.setData({
searchsuggest:res.result.allMatch
})
}
})
}
點(diǎn)擊熱搜或歷史,執(zhí)行搜索
思路:關(guān)鍵是
event,點(diǎn)擊通過(guò)e.currentTarget.dataset.value拿到所點(diǎn)擊的值,再交給其他方法執(zhí)行搜索行為。
// 點(diǎn)擊熱門(mén)搜索值或搜索歷史,填入搜索框
fill_value:function(e){
let that = this;
console.log(history)
// console.log(e.currentTarget.dataset.value)
that.setData({
searchKey: e.currentTarget.dataset.value,//點(diǎn)擊吧=把值給searchKey,讓他去搜索
inputValue: e.currentTarget.dataset.value,//在輸入框顯示內(nèi)容
showView:false,//給false值,隱藏 熱搜和歷史 界面
showsongresult: false, //給false值,隱藏搜索建議頁(yè)面
})
that.searchResult(); //執(zhí)行搜索功能
}
搜索結(jié)果
思路:輸入結(jié)束->確認(rèn)鍵->調(diào)用
searchResult請(qǐng)求到結(jié)果
// 搜索完成點(diǎn)擊確認(rèn)
searchover:function(){
let that = this;
that.setData({
showsongresult: false
})
that.searchResult();
}
// 搜索結(jié)果
searchResult(){
console.log(this.data.searchKey)
API.searchResult({ keywords: this.data.searchKey, type: 1, limit: 100, offset:2 }).then(res => {
if (res.code === 200) {
this.setData({
searchresult: res.result.songs
})
}
})
}
樂(lè)庫(kù)部分
樂(lè)庫(kù)部分其實(shí)沒(méi)什么邏輯很難的部分,以結(jié)構(gòu)和樣式為主,在這里就不贅述了??梢缘轿业?a target="_blank">github上查看。在這里分享一些小功能的實(shí)現(xiàn)和踩到的坑。
個(gè)性推薦,主播電臺(tái)切換
1.個(gè)性推薦和主播電臺(tái)是兩個(gè)
swiper-item所以他們才可以左右滑動(dòng),就像輪播圖一樣,不過(guò)輪播圖放的是圖片,而這里放的是整個(gè)頁(yè)面。2.我要實(shí)現(xiàn)的效果是左右滑動(dòng)的同時(shí),
個(gè)性推薦和主播電臺(tái)下面的白色方塊也要跟著滑動(dòng)。1. 第一種方法
給包裹兩個(gè)
swiper-item的swiper添加一個(gè)bindchange="changeline"事件,把事件對(duì)象event打印出來(lái)發(fā)現(xiàn),console.log(e.detail.current),當(dāng)我們左右滑動(dòng)的時(shí)候cuurrent的值會(huì)在0和1之間切換。所以我給白色方塊添加
class="{{changeline?'swiper_header_line_before':'swiper_header_line_after'}}"
if(e.detail.current === 0){
this.setData({
changeline:true
})
}else{
this.setData({
changeline:false
})
}
當(dāng)current為0,即頁(yè)面在個(gè)性推薦時(shí),讓changeline為true;當(dāng)current為1,即頁(yè)面在主播電臺(tái)時(shí),讓changeline為false;為true時(shí),給白色方塊加持swiper_header_line_before的樣式,為false時(shí),加持swiper_header_line_after的樣式。這樣就可以跟隨swiper-item的滑動(dòng)而切換了。但是,這種切換方式太僵硬了,沒(méi)有那種流暢的切換效果,而且不適合多swiper-item頁(yè)面。
2. 第二種方法
讓一半寬度,四分之一寬度設(shè)置為變量是為了兼容不同的手機(jī)型號(hào)。因?yàn)閷?xiě)死數(shù)據(jù)肯定會(huì)有BUG,所以才要計(jì)算寬度。
<view class="weui-navbar-slider" style="transform:translateX({{slideOffset}}px);"></view>
.weui-navbar-slider{
width:28px;
height: 5px;
background: #ffffff;
border-radius:10rpx;
transition: transform .6s;
}
slideOffset為變量,動(dòng)態(tài)接受從data傳來(lái)的數(shù)據(jù)。
onLoad:function(){
wx.getSystemInfo({
success: function (res) {
// console.log(res.windowWidth)
// console.log(res.windowWidth / 2 / 2)
half = res.windowWidth / 2 ;
quarter = res.windowWidth / 2 / 2;
that.setData({
slideOffset: quarter - 14 //onLoad的時(shí)候讓 quarter - 14 給slideOffset,即一開(kāi)始就讓他在個(gè)性推薦的下面,否則onLoad的時(shí)候一開(kāi)始在0的位置
})
}
})
}
changeline:function(e){
// console.log(e)
// console.log(e.detail.current)
let current = e.detail.current; //獲取swiper的current值
if(e.detail.current === 0){
this.setData({
slideOffset: quarter - 14
})
}
if(e.detail.current === 1){
this.setData({
slideOffset: (quarter - 14) + half
})
}
if(e.detail.current === null){
this.setData({
slideOffset: quarter - 14
})
}
}
MV播放
主要是結(jié)構(gòu)和樣式,我直接上代碼了。
<!-- play_mv.wxml -->
<view class="mv_box">
<video src="{{mv.brs['480']}}" class="mv" autoplay="{{autoplay}}" loop="{{loop}}" direction="{{0}}" show-fullscreen-btn="{{showfullscreenbtn}}"
show-center-play-btn="{{showcenterplaybtn}}" enable-progress-gesture="{{enableprogressgesture}}" show-mute-btn="{{showmutebtn}}" title="{{mv.name}}"
play-btn-position="{{center}}" object-fit="{{objectfit}}"></video>
</view>
<view class="mv_name">{{mv.name}}</view>
<view class="mv_time"> 發(fā)行: {{mv.publishTime}}</view>
<view class="mv_time mv_times">播放次數(shù): {{mv.playCount}}</view>
<view class="mv_time mv_desc">{{mv.desc}}</view>
<view class="mv_time mv_desc mv_other">點(diǎn)贊: {{mv.likeCount}}</view>
<view class="mv_time mv_desc mv_other">收藏: {{mv.subCount}}</view>
<view class="mv_time mv_desc mv_other">評(píng)論: {{mv.commentCount}}</view>
<view class="mv_time mv_desc mv_other">分享: {{mv.shareCount}}</view>
/* play/play_mv.wxss */
.mv_box{
width: 100%;
height: 480rpx;
margin-top:-2rpx;
}
.mv{
width: 100%;
height: 100%;
border-radius:15rpx;
}
.mv_name{
margin-top:20rpx;
margin-left:20rpx;
}
.mv_time{
font-size: 12px;
margin-left:20rpx;
color:#979798;
display:initial;
}
.mv_times{
margin-left: 100rpx;
}
.mv_desc{
display: block;
color:#6A6B6C;
}
.mv_other{
display: block;
}
// play_mv.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
data: {
mv: [],
autoplay: true,
loop: true,
showfullscreenbtn: true,
showcenterplaybtn: true,
enableprogressgesture: true,
showmutebtn: true,
objectfit: 'contain',
},
onLoad: function (options) {
// console.log(mv_url);
const mvid = options.id; // onLoad()后獲取到歌曲視頻之類(lèi)的id
// 請(qǐng)求MV的地址,失敗則播放出錯(cuò),成功則傳值給createBgAudio(后臺(tái)播放管理器,讓其后臺(tái)播放)
wx.request({
url: API_BASE_URL + '/mv/detail',
data: {
mvid: mvid
},
success: res => {
console.log(res.data.data.brs['480'])
console.log('歌曲音頻url:', res)
if (res.data.data.brs === null) { //如果是MV 電臺(tái) 廣告 之類(lèi)的就提示播放出錯(cuò),并返回首頁(yè)
console.log('播放出錯(cuò)')
wx.showModal({
content: '服務(wù)器開(kāi)了點(diǎn)小差~~',
cancelColor: '#DE655C',
confirmColor: '#DE655C',
showCancel: false,
confirmText: '返回',
complete() {
wx.switchTab({
url: '/pages/index/index'
})
}
})
} else {
this.setData({
mv: res.data.data
})
}
}
})
},
})
歌手榜
// 歌手榜的js
const API = require('../../API/api');
const app = getApp();
Page({
data: {
songers: [], //歌手榜
},
onLoad: function (options) {
wx.showLoading({
title: '加載中',
});
this.getSonger();
},
getSonger: function () {
API.getSonger({}).then(res => {
wx.hideLoading()
this.setData({
songers: res.list.artists.slice(0, 100)
})
})
},
handleSheet: function (event) { //event 對(duì)象,自帶,點(diǎn)擊事件后觸發(fā),event有type,target,timeStamp,currentTarget屬性
const sheetId = event.currentTarget.dataset.id; //獲取到event里面的歌曲id賦值給audioId
wx.navigateTo({ //獲取到id帶著完整url后跳轉(zhuǎn)到play頁(yè)面
url: `./moremore_songer?id=${sheetId}`
})
},
})
<!-- 歌手榜結(jié)構(gòu) -->
<view wx:for="{{songers}}" wx:key="" class='songer_box' data-id="{{item.id}}" bindtap='handleSheet'>
<view class='songer_index_box'>
<text class='songer_index'>{{index + 1}}</text>
</view>
<view class='songer_img_box'>
<image src="{{item.picUrl}}" class='songer_img'></image>
</view>
<view class='songer_name_box'>
<text class='songer_name'>{{item.name}}</text>
<text class='songer_score'>{{item.score}}熱度</text>
</view>
</view>
// 歌手下級(jí)路由歌曲列表
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
data: {
songList: []
},
onLoad: function (options) {
wx.showLoading({
title: '加載中',
});
const sheetId = options.id;
wx.request({
url: API_BASE_URL + '/artists',
data: {
id: sheetId
},
success: res => {
const waitForPlay = new Array;
for (let i = 0; i <= res.data.hotSongs.length - 1; i++) { //循環(huán)打印出其id
waitForPlay.push(res.data.hotSongs[i].id) //循環(huán)push ID 到waitForPlay數(shù)組
app.globalData.waitForPlaying = waitForPlay //讓waitForPlay數(shù)組給全局?jǐn)?shù)組
// console.log(app.globalData.waitForPlaying)
}
wx.hideLoading()
console.log(res.data.hotSongs)
this.setData({
songList: res.data.hotSongs
})
}
})
},
handlePlayAudio: function (event) { //event 對(duì)象,自帶,點(diǎn)擊事件后觸發(fā),event有type,target,timeStamp,currentTarget屬性
const audioId = event.currentTarget.dataset.id; //獲取到event里面的歌曲id賦值給audioId
wx.navigateTo({ //獲取到id帶著完整url后跳轉(zhuǎn)到play頁(yè)面
url: `../../play/play?id=${audioId}`
})
}
})
<!-- more/more_songer/moremore_songer.wxml歌手下面的歌曲 -->
<view class='search_result_songs'>
<view wx:for="{{songList}}" wx:key="" class='search_result_song_item songer_box' data-id="{{item.id}}" bindtap='handlePlayAudio'>
<view class='songer_index_box'>
<text class='songer_index'>{{index + 1}}</text>
</view>
<view class='songer_img_box'>
<view class='search_result_song_song_name'>{{item.name}}</view>
<view class='search_result_song_song_art-album'>{{item.ar[0].name}} - {{item.al.name}}</view>
</view>
</view>
</view>
推薦歌單
因?yàn)闃邮脚c排行榜類(lèi)似,所以只放出圖片,源碼可以到我的github上查看。
榜單排行
請(qǐng)查看源碼
換一換功能
思路:綁定點(diǎn)擊事件->選取隨機(jī)的三個(gè)數(shù)->給空值->push三個(gè)隨機(jī)數(shù)進(jìn)數(shù)組中->重新賦值。
// 換一換
change_1:function(){
let maxNum = this.data.more_recommend_create.length //計(jì)算數(shù)據(jù)長(zhǎng)度
let r1 = parseInt(Math.random() * (maxNum - 0) + 0); //取【0-數(shù)據(jù)長(zhǎng)度】?jī)?nèi)的整數(shù)隨機(jī)數(shù)
let r2 = parseInt(Math.random() * (maxNum - 0) + 0);
let r3 = parseInt(Math.random() * (maxNum - 0) + 0);
this.setData({
recommend_create: []
})
//重新取3組數(shù)據(jù)
this.data.recommend_create.push(this.data.more_recommend_create[r1])
this.data.recommend_create.push(this.data.more_recommend_create[r2])
this.data.recommend_create.push(this.data.more_recommend_create[r3])
//重新賦值
this.setData({
recommend_create: this.data.recommend_create
})
}
播放界面
圖片太大,因此加快了播放。
播放功能
思路:利用data-id="{{item.id}}"獲取到歌曲ID放在event中-> 通過(guò)event對(duì)象事件獲取ID并跳轉(zhuǎn)到播放頁(yè)面 ->wx.request獲取到歌曲的音頻地址及detail->背景音頻管理器 wx.getBackgroundAudioManager()->播放
以歌手榜下級(jí)路由歌曲列表為例,
<view wx:for="{{songList}}" wx:key="" class='search_result_song_item songer_box' data-id="{{item.id}}" bindtap='handlePlayAudio'>
handlePlayAudio: function (event) { //event 對(duì)象,自帶,點(diǎn)擊事件后觸發(fā),event有type,target,timeStamp,currentTarget屬性
const audioId = event.currentTarget.dataset.id; //獲取到event里面的歌曲id賦值給audioId
wx.navigateTo({ //獲取到id帶著完整url后跳轉(zhuǎn)到play頁(yè)面
url: `../../play/play?id=${audioId}`
})
}
// play.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
data: {
isPlay: '',
song:[],
innerAudioContext: {},
show:true,
showLyric:true,
songid:[],
history_songId:[]
},
onLoad: function (options) {
const audioid = options.id; // onLoad()后獲取到歌曲視頻之類(lèi)的id
this.play(audioid); //把從wxml獲取到的值傳給play()
},
play: function (audioid){
const audioId = audioid;
app.globalData.songId = audioId; //讓每一個(gè)要播放的歌曲ID給全局變量的songId
const innerAudioContext = wx.createInnerAudioContext();
this.setData({
innerAudioContext,
isPlay: true
})
// 請(qǐng)求歌曲音頻的地址,失敗則播放出錯(cuò),成功則傳值給createBgAudio(后臺(tái)播放管理器,讓其后臺(tái)播放)
wx.request({
url: API_BASE_URL + '/song/url',
data: {
id: audioId
},
success: res => {
if (res.data.data[0].url === null) { //如果是MV 電臺(tái) 廣告 之類(lèi)的就提示播放出錯(cuò),并返回首頁(yè)
wx.showModal({
content: '服務(wù)器開(kāi)了點(diǎn)小差~~',
cancelColor: '#DE655C',
confirmColor: '#DE655C',
showCancel: false,
confirmText: '返回',
complete() {
wx.switchTab({
url: '/pages/index/index'
})
}
})
} else {
this.createBgAudio(res.data.data[0]);
}
}
})
//獲取到歌曲音頻,則顯示出歌曲的名字,歌手的信息,即獲取歌曲詳情;如果失敗,則播放出錯(cuò)。
wx.request({
url: API_BASE_URL + '/song/detail',
data: {
ids: audioId //必選參數(shù)ids
},
success: res => {
if (res.data.songs.length === 0) {
wx.showModal({
content: '服務(wù)器開(kāi)了點(diǎn)小差~~',
cancelColor: '#DE655C',
confirmColor: '#DE655C',
showCancel: false,
confirmText: '返回',
complete() {
wx.switchTab({
url: '/pages/index/index'
})
}
})
} else {
this.setData({
song: res.data.songs[0], //獲取到歌曲的詳細(xì)內(nèi)容,傳給song
})
app.globalData.songName = res.data.songs[0].name;
}
},
})
},
createBgAudio(res) {
const bgAudioManage = wx.getBackgroundAudioManager(); //獲取全局唯一的背景音頻管理器。并把它給實(shí)例bgAudioManage
app.globalData.bgAudioManage = bgAudioManage; //把實(shí)例bgAudioManage(背景音頻管理器) 給 全局
bgAudioManage.title = 'title'; //把title 音頻標(biāo)題 給實(shí)例
bgAudioManage.src = res.url; // res.url 在createBgAudio 為 mp3音頻 url為空,播放出錯(cuò)
const history_songId = this.data.history_songId
const historySong = {
id: app.globalData.songId,
songName:app.globalData.songName
}
history_songId.push(historySong)
bgAudioManage.onPlay(res => { // 監(jiān)聽(tīng)背景音頻播放事件
this.setData({
isPlay: true,
history_songId
})
});
bgAudioManage.onEnded(() => { //監(jiān)聽(tīng)背景音樂(lè)自然結(jié)束事件,結(jié)束后自動(dòng)播放下一首。自然結(jié)束,調(diào)用go_lastSong()函數(shù),即歌曲結(jié)束自動(dòng)播放下一首歌
this.go_lastSong();
})
wx.setStorageSync('historyId', history_songId); //把historyId存入緩存
},
})
暫停/播放
<!-- 暫停播放圖標(biāo) -->
<view class="play_suspend">
<view class="icon_playing"><image bindtap="handleToggleBGAudio" src="../images/suspend.png" hidden="{{!isPlay}}" class="{{'img_play_suspend'}}" /> <!-- 暫停圖標(biāo)-->
<image bindtap="handleToggleBGAudio" src="../images/play.png" hidden="{{isPlay}}" class="{{'img_play_suspend'}}" /></view> <!--播放圖標(biāo)-->
</view>
// 播放和暫停
handleToggleBGAudio() {
// const innerAudioContext = app.globalData.innerAudioContext;
const bgAudioManage = app.globalData.bgAudioManage;
const {isPlay} = this.data;
if (isPlay) {
bgAudioManage.pause();
// innerAudioContext.pause();handleToggleBGAudio
} else {
bgAudioManage.play();
// innerAudioContext.play();
}
this.setData({
isPlay: !isPlay
})
console.log(this.data.isPlay)
}
上一首/下一首(隨機(jī)播放)
思路:點(diǎn)擊歌單或歌手頁(yè),獲取到對(duì)應(yīng)的歌單/歌手id->wx.request請(qǐng)求數(shù)據(jù)獲取到所有的歌單內(nèi)/歌手熱門(mén)歌曲音頻地址->給全局變量globalData->點(diǎn)擊上一首/下一首隨機(jī)獲取到全局變量的一則數(shù)據(jù)->給play()方法->播放
<!--歌單-->
onLoad: function (options) {
wx.showLoading({
title: '加載中',
});
const sheetId = options.id;
wx.request({
url: API_BASE_URL + '/playlist/detail',
data: {
id: sheetId
},
success: res => {
const waitForPlay = new Array;
for (let i = 0; i <= res.data.playlist.trackIds.length - 1;i++){ //循環(huán)打印出其id
waitForPlay.push(res.data.playlist.trackIds[i].id) //循環(huán)push ID 到waitForPlay數(shù)組
app.globalData.waitForPlaying = waitForPlay //讓waitForPlay數(shù)組給全局?jǐn)?shù)組
}
wx.hideLoading()
this.setData({
songList: res.data.playlist.tracks
})
}
})
}
<view class="icon_playing "><image src="../images/lastSong.png" class=" icon_play" bindtap="go_lastSong" /></view>
<view class="icon_playing "><image src="../images/nextSong.png" class=" icon_play" bindtap="go_lastSong" /></view>
go_lastSong:function(){
let that = this;
const lastSongId = app.globalData.waitForPlaying;
const songId = lastSongId[Math.floor(Math.random() * lastSongId.length)]; //隨機(jī)選取lastSongId數(shù)組的一個(gè)元素
that.data.songid = songId;
this.play(songId)//傳進(jìn)play()方法中
app.globalData.songId=songId;
}
歌詞/封面切換
因?yàn)榫W(wǎng)易云API的歌詞接口崩潰,請(qǐng)求不到歌詞,所以我只能把歌詞寫(xiě)死為
純音樂(lè),請(qǐng)欣賞。類(lèi)似于v-show。
<!-- 封面 -->
<!-- 一開(kāi)始o(jì)nload時(shí),showLyric=true, 顯示為轉(zhuǎn)動(dòng)的圖標(biāo),點(diǎn)擊圖標(biāo),切換為歌詞-->
<view class="sing-show" bindtap="showLyric" >
<view class="moveCircle {{isPlay ? 'play' : ''}}" hidden="{{!showLyric}}">
<image src="{{song.al.picUrl}}" class="coverImg {{isPlay ? 'play' : ''}}" hidden="{{!showLyric}}"/>
</view>
<text hidden="{{showLyric}}" class="songLyric">純音樂(lè),請(qǐng)欣賞</text>
</view>
// 點(diǎn)擊切換歌詞和封面
showLyric(){
const {showLyric} = this.data;
this.setData({
showLyric: !showLyric
})
}
破產(chǎn)版的孤獨(dú)星球動(dòng)效
封面旋轉(zhuǎn):
@keyframes rotate {
0%{
transform: rotate(0);
}
100%{
transform: rotate(360deg);
}
}
擴(kuò)散的圓形線(xiàn)條:
其實(shí)就是外面套一個(gè)盒子,盒子寬高變大以及透明度逐漸變低。
@keyframes moveCircle {
0%{
width: 400rpx;
height: 400rpx;
border: 1px solid rgba(255, 255, 255, 1)
}
30%{
width: 510rpx;
height: 510rpx;
border: 1px solid rgba(255, 255, 255, 0.8)
}
50%{
width: 610rpx;
height: 610rpx;
border: 1px solid rgba(255, 255, 255, 0.6)
}
80%{
width: 700rpx;
height: 700rpx;
border: 1px solid rgba(255, 255, 255, 0.4)
}
99%{
width: 375px;
height: 375px;
border: 1px solid rgba(255, 255, 255, 0.1)
}
100%{
width: 0px;
height: 0px;
border: 1px solid rgba(255, 255, 255, 0)
}
}
背景毛玻璃
<!-- play.wxml -->
<image src="{{song.al.picUrl}}" class="background_img" ></image>
/* 播放界面毛玻璃效果 */
.background_img{
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
filter: blur(20px);
z-index: -1;
transform: scale(1.5); /*和網(wǎng)易云音樂(lè)對(duì)比了一下,發(fā)現(xiàn)也是放大1.5倍*/
}
播放tabBar
思路是參考酷狗音樂(lè)小程序。這個(gè)tabBar的js,wxml與播放功能界面的js,wxml相同。因?yàn)橐魳?lè)播放是用
wx.getBackgroundAudioManager()背景音頻播放器管理的,所以才能同步。
我的tabBar
播放歷史
思路:play.js中一旦播放成功就把歌名及歌曲ID傳入全局變量->push到play.js里的數(shù)組中->wx.setStorageSync把數(shù)據(jù)存入緩存->在需要的頁(yè)面wx.getStorageSync獲取到緩存。
<!--play.js-->
const history_songId = this.data.history_songId
const historySong = {
// id: res.id
id: app.globalData.songId,
songName:app.globalData.songName
}
history_songId.push(historySong)
wx.setStorageSync('historyId', history_songId); //把historyId存入緩存
<!--me.js-->
onShow:function(){
var history = wx.getStorageSync('historyId');
// console.log(history)
this.setData({
hidden:true,
// historyId: app.globalData.songName
historyId: history
})
console.log(this.data.historyId)
}
結(jié)語(yǔ)
做項(xiàng)目的過(guò)程總的來(lái)說(shuō)痛并快樂(lè),因?yàn)楦牟怀鯞UG的樣子真的很狼狽,但實(shí)現(xiàn)了某一個(gè)功能的那一刻真的很欣慰。再次感謝給予幫助的老師同學(xué)。如果你喜歡這篇文章或者可以幫到你,不妨點(diǎn)個(gè)贊吧!同時(shí)也非常希望看到這篇文章的你在下方給出建議!最后奉上源碼,有需要的可以自取。最后,說(shuō)點(diǎn)題外話(huà),因?yàn)槲沂?020屆畢業(yè)生,現(xiàn)在面臨實(shí)習(xí)壓力,有沒(méi)有大佬撈一下。