最近一直在开发一个类似于小年糕的微信小程序,在开发制作MV功能时 ,花费了一些心思,其间主要遇到了以下一些问题点:
1. 上传图片的动画效果如何像播放视频一样实现播放与暂停?
2. 用户上传的图片数量不确定,在音乐没有播完之前,上传图片太多或太少将如何处理?
3. 如何让展现的歌词与当前播放的那一句保持同步,即唱哪一句就显示哪一句?
4. 当前音乐的播放时间如何与自定义进度条的进度保持一致?
针对以上问题,首先我们来看一下实现的效果,
下面我们来一一解答以上提出的问题点:
第一个问题,动画如何暂停与播放,我采用了animation-play-state 这个属性来控制动作的播放与暂停,当它的值为 play 时,动画会播放,如果值为 paused 时,动画会暂停。
第二个问题,当音乐还在播放时,用户上传的图片如果太少,将图片进行了循环展示,直到音乐播放完毕,由于整个MV的时长取决于所选择音乐的时长,如果上传的图片太多,当音乐播放完毕时,我将后面的图片进行了省略处理。
第三个问题,为了让歌词展示与音乐播放保持同步,我对音乐的歌词格式进行了处理,将每一句歌词与该歌词的播放时间分别组成一个对象,然后将多个对象组成一个数组,将数组循环展示在页面上,其中时间格式为是整型的秒数。在播放时,将每一句歌词的播放时间与音乐当前播放的时间进行了对比,如果歌词的播放时间大于等于当前音乐播放时间,并且小于一下句歌词的播放时间就显示该歌词,否则不显示。
第四个问题,为了让播放时间与进度条的进度同步,我利用了 onTimeUpdate 这个API,即监听音乐播放时间更新的函数,在这个API的回调函数里,获取当前音乐的动态播放时间,将播放时间与音乐的总时间相除,即可得出播放时间的百分比,然后将这个比值乘以100进行向上取整,最后将该结果赋给进度条 slider 的 value 值,这样就可以实现音乐在播的同时进度条同步更新的效果。
以上就是问题的解决方案,下面是实际的代码,在实际的代码中,我还加入了拖拽进度条快进或快退、页面显示、页面隐藏、页面销毁时对音乐播放器的一些处理。
WXML代码:
<view style="height:calc(100vh - {{height}}rpx - 202rpx)" class="mv-box">
<!-- MV动画 -->
<block>
<view class="mv" bindtap="showPlayControl">
<image class="mv-background" src="{{mvImageSrc[mvImgIndex]}}" style="animation:{{animationArray[mvImgIndex%4]}} 5s linear 1 both;animation-play-state:{{mvStatus}};" wx:if="{{mvImageSrc.length}}"></image>
</view>
<view class="mv" bindtap="showPlayControl">
<image class="mv-image" wx:for="{{mvImageSrc}}" wx:key="index" src="{{item}}" style="animation:{{animationArray[index%4]}} 5s linear 1 both;animation-play-state:{{mvStatus}};" bindanimationend='singleMvOver' wx:if="{{mvImageSrc.length && mvImgIndex==index?true:false}}"
mode="widthFix">
</image>
</view>
</block>
<!-- MV歌词 -->
<view class="lyric" wx:for="{{mvMusicInfo.lrcText}}" wx:key="index" wx:if="{{mvMusicInfo.lrcText.length && index < mvMusicInfo.lrcText.length && mvMusicInfo.lrcText[index][0]<= sliderStartTimeNum && sliderStartTimeNum < mvMusicInfo.lrcText[index + 1][0]}}">
{{item[1]}}
</view>
<!-- 控制条 -->
<view class="play-control" hidden="{{!showPlayControl}}">
<view class="play-control-left" bindtap="changeMvStatus">
<image src="/images/play-white-40.png" wx:if="{{mvIsPlay}}"></image>
<image src="/images/pause-white-40.png" wx:if="{{!mvIsPlay}}"></image>
</view>
<view class="play-control-right" bindtap="clickSlider">
<text class="start-time">{{sliderStartTime}}</text>
<slider block-size="16" value="{{sliderValue}}" backgroundColor="#838383" activeColor='#ffffff' bindchanging="dragingEvent" bindchange="dragEndEvent"></slider>
<text class="end-time">{{sliderEndTime}}</text>
</view>
</view>
</view>
WXSS代码
/* 外层容器 */
.mv-box {
width: 100%;
background-color: #fff;
display: flex;
position: relative;
z-index: 25;
}
/* MV动画 */
.mv {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
.mv-background {
filter: blur(10px);
height: 100%;
}
.mv-image {
position: absolute;
}
/* 放大动画 */
@keyframes enlarge {
0% {
width: 750rpx;
transform: scale(1);
}
100% {
width: 750rpx;
transform: scale(1.2);
opacity: 0.3;
}
}
/* 缩小动画 */
@keyframes shrink {
0% {
width: 750rpx;
transform: scale(1.2);
}
100% {
width: 750rpx;
transform: scale(1);
opacity: 0.3;
}
}
/* 左移动画 */
@keyframes moveLeft {
0% {
width: 850rpx;
left: 0rpx;
}
100% {
width: 850rpx;
left: -100rpx;
opacity: 0.3;
}
}
/* 右移动画 */
@keyframes moveRight {
0% {
width: 850rpx;
left: -100rpx;
}
100% {
width: 850rpx;
left: 0;
opacity: 0.3;
}
}
/* MV歌词 */
.lyric {
position: absolute;
bottom: 80rpx;
left: 0;
width: calc(100% - 60rpx);
padding: 0 30rpx;
line-height: 80rpx;
z-index: 30;
text-align: center;
font-size: 40rpx;
font-weight: 600;
color: #ff2066;
}
/* 控制条 */
.play-control {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 80rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: #030303;
z-index: 30;
}
.play-control-left {
width: 80rpx;
height: 80rpx;
}
.play-control-left image {
width: 40rpx;
height: 40rpx;
padding: 20rpx;
}
.play-control-right {
width: 670rpx;
height: 80rpx;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.play-control-right .start-time {
display: inline-block;
height: 40rpx;
line-height: 40rpx;
width: 70rpx;
color: #fff;
font-size: 22rpx;
text-align: center;
}
.play-control-right slider {
width: 490rpx;
}
.play-control-right .end-time {
display: inline-block;
height: 40rpx;
line-height: 40rpx;
width: 90rpx;
color: #fff;
font-size: 22rpx;
text-align: left;
}
JS代码
Page({
data: {
// 系统状态栏高度
// 实际动态获取
height: 40,
// MV轮播的图片
mvImageSrc: [],
// 当前轮播图片的下标
mvImgIndex: 0,
// MV轮播的动画效果
animationArray: ['enlarge', 'shrink', 'moveLeft', 'moveRight'],
// MV动画的播放状态
mvStatus: 'play',
// 音乐信息
mvMusicInfo: {},
// 显示音频播放进度条
showPlayControl: true,
// 控制条暂停与播放的图标
mvIsPlay: true,
// 音乐正在播放时间
sliderStartTime: '00:00',
// 音乐正在播放时间秒数
sliderStartTimeNum: 0,
// 当前控制条播放进度
sliderValue: 0,
// 音乐播放总时长
sliderEndTime: '00:00',
// 音乐播放总秒数
sliderEndTimeNum: 0,
},
// 页面加载时
onLoad: function(options) {
// 创建音乐播放器
var audioCtx = wx.createInnerAudioContext();
this.setData({
audioCtx: audioCtx
}, function() {
this.getData();
})
},
// 获取图片和音乐信息
getData: function() {
// 模拟请求获取MV相关信息
// MV的图片(图片地址已作处理,非真实有效)
var mvImageSrc = [
"https://qnybd.xxxxx.cn/44b76201910210912393964.jpg",
"https://qnybd.xxxxx.cn/7e33520191030155441391.png",
"https://qnybd.xxxxx.cn/43c96201910301553019375.png"
];
// 音乐信息
var mvMusicInfo = {
// 音乐地址(音乐地址已作处理,非真实有效)
sourcePath: 'https://qnybd.xxxxx.cn/d0d6a8eacd9ec492ba0b5424335bfb4e.mp3',
// 音乐歌词(由于歌词太多,此处仅做部分示例)
lrcText: "[00:00.860]作词:杨丽丽\n[00: 02.580] 混音:周佳佳\n[00:04.480] 编曲:王悦悦",
// 音乐时长
musicTime: '04:21'
};
// 添加音乐地址
var audioCtx = this.data.audioCtx;
audioCtx.src = mvMusicInfo.sourcePath;
audioCtx.play();
// 播放状态,绑定播放进度更新事件,控制进度条和时间显示
// onTimeUpdate在
audioCtx.onTimeUpdate(this.timeUpdate);
// 处理音乐歌词的格式
mvMusicInfo.lrcText = this.parseLyric(mvMusicInfo.lrcText);
// 播放器控件的最大时长
var sliderEndTime = mvMusicInfo.musicTime;
var sliderEndTimeNum = this.formatTimeToNum(sliderEndTime);
// 更新页面信息
this.setData({
mvImageSrc: mvImageSrc,
mvMusicInfo: mvMusicInfo,
sliderEndTime: sliderEndTime,
sliderEndTimeNum: sliderEndTimeNum,
audioCtx: audioCtx
})
},
// 滑块拖动过程中的事件
dragingEvent: function(e) {
var that = this;
// 暂停播放
that.data.audioCtx.stop();
// 取消监听播放进度
that.data.audioCtx.offTimeUpdate();
that.setData({
mvIsPlay: false,
mvStatus: 'paused',
});
},
// 拖动完成后
dragEndEvent: function(e) {
var that = this;
// 总时间
var sliderEndTimeNum = that.data.sliderEndTimeNum;
// 拖动百分比
var percent = e.detail.value / 100;
// 计算拖拽时的播放时间
var sliderStartTimeNum = Math.ceil(sliderEndTimeNum * percent);
var sliderStartTime = that.formatTimeToStr(sliderStartTimeNum);
that.setData({
mvIsPlay: true,
mvStatus: 'play',
sliderStartTimeNum: sliderStartTimeNum,
sliderStartTime: sliderStartTime,
sliderValue: e.detail.value
},function(){
// 跳转时间
that.data.audioCtx.seek(that.data.sliderStartTimeNum);
// 播放音乐
that.data.audioCtx.play();
// 监听进度
that.data.audioCtx.onTimeUpdate(that.timeUpdate);
});
},
// 处理歌词格式
parseLyric: function(text) {
// 按换行切割
var text = text.replace(/\\n/g,'\n');
text = text.replace(/\n/g, '===');
var lines = text.split('===');
// 筛选歌词,修正时间
let list = [];
for (let b = 0; b < lines.length; b++) {
let item = lines[b];
// 拆分与切割
item = item.replace(/\[/g, '');
item = item.split(']');
// 去掉有时间的空歌词
if (item[item.length - 1]) {
for (let j = 0; j < item.length - 1; j++) {
// 去掉时间的毫秒数
let index = item[j].indexOf('.');
// 截取分和秒并替换分后面的空格
item[j] = item[j].substring(0, index).replace(/\s+/g, '');
}
list.push(item);
};
};
// 处理歌词多个时间共享的问题
let arr = [];
for (let k = 0; k < list.length; k++) {
if (list[k].length == 2) {
list[k][0] = App.formatTimeToNum(list[k][0]);
arr.push(list[k]);
} else {
for (let n = 0; n < (list[k].length - 1); n++) {
let obj = [
// 时间
App.formatTimeToNum(list[k][n]),
// 共用的歌词
list[k][list[k].length - 1],
];
arr.push(obj);
}
}
};
// 对歌词按时间排序
arr.sort(function (a, b) {
return a[0] - b[0];
});
return arr;
},
// 页面影藏
onHide: function() {
this.setData({
mvIsPlay: false,
mvStatus: 'paused'
});
this.data.audioCtx.pause();
},
// 页面显示
onShow: function() {
this.setData({
mvIsPlay: true,
mvStatus: 'play'
});
if (this.data.audioCtx) {
this.data.audioCtx.play();
};
},
// 页面卸载
onUnload: function() {
this.data.audioCtx.destroy();
},
// 播放的时候,更新进度条和时间显示
timeUpdate: function() {
let that = this;
// 当前播放的秒数
let sliderStartTimeNum = Math.ceil(that.data.audioCtx.currentTime);
let sliderValue = Math.round(sliderStartTimeNum / that.data.audioCtx.duration * 100);
if (sliderValue == 100) {
that.data.audioCtx.pause();
that.setData({
// 切换为暂停按钮
mvIsPlay: false,
// 停止动画
mvStatus: 'paused',
// 图片换为第一张
mvImgIndex: 0,
// 音乐播放时间归0
sliderStartTime: '00:00',
// 音乐播放时间归0
sliderStartTimeNum: 0,
// 当前播放进度
sliderValue: 0,
});
} else {
that.setData({
sliderStartTime: that.formatTimeToStr(sliderStartTimeNum),
sliderStartTimeNum: sliderStartTimeNum,
sliderValue: sliderValue
})
}
},
// 将时间秒数转为字符串
formatTimeToStr: function(num) {
var a = Math.floor(num / 600);
var b = Math.floor(num % 600 / 60);
var c = Math.floor((num - a * 600 - b * 60) / 10);
var d = parseInt((num - a * 600 - b * 60) % 10);
var timeStr = '' + a + b + ':' + c + d;
return timeStr
},
// 将时间格式转为秒数
formatTimeToNum: function(str) {
var first = str.substring(0, 2);
var end = str.substring(3, 5);
var allTimeStr = first + end;
var allTimeArry = allTimeStr.split('');
var allTimeNum = 0;
for (var i = 0; i < allTimeArry.length; i++) {
if (i == 0) {
allTimeNum += parseInt(allTimeArry[i] * 600);
} else if (i == 1) {
allTimeNum += parseInt(allTimeArry[i] * 60);
} else if (i == 2) {
allTimeNum += parseInt(allTimeArry[i] * 10);
} else if (i == 3) {
allTimeNum += parseInt(allTimeArry[i]);
}
};
return allTimeNum
},
// 显示播放进度条
showPlayControl: function(e) {
this.setData({
showPlayControl: true
})
},
// 单个图片播放结束时
singleMvOver: function() {
var that = this;
var mvImgIndex = that.data.mvImgIndex + 1;
if (mvImgIndex == that.data.mvImageSrc.length) {
that.setData({
mvImgIndex: 0
});
} else {
that.setData({
mvImgIndex: mvImgIndex
})
}
},
// 暂停或播放MV
changeMvStatus: function() {
var that = this;
// 获取当前正在播放的状态
var isPlay = that.data.mvIsPlay;
if (isPlay) {
// 暂停音乐
that.data.audioCtx.pause();
this.setData({
mvIsPlay: false,
mvStatus: 'paused',
});
} else {
// 播放
that.data.audioCtx.play();
this.setData({
mvIsPlay: true,
mvStatus: 'play',
});
};
},
// 点击滑动滑块
clickSlider: function(e) {
this.setData({
showPlayControl: false
})
},
})