在移动应用开发中,音乐播放器是一个常见的需求。本文将介绍如何使用UniApp框架封装一个自定义的音乐播放器组件,简单且轻量级,完美支持vue2/vue3,支持H5、小程序、Android等平台,且可以任意定制化。网上找了一大堆插件都不太满意,不如使用内置的uni.createInnerAudioContext()接口自己实现,封装成了小组件的形式方便复用。
以下我们将通过详细的代码和解释,帮助你理解如何构建一个功能齐全的音乐播放器。
组件代码解析
模板部分
首先,我们来看一下组件的模板部分:
标题和副标题为了实现滚动效果,使用了 uni-ui自带的uni-notice-bar组件。进度条部分使用slider组件实现。
<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-icons v-show="isCollectBtn" @click="handleCollec" type="heart" size="20" style="color:#848484; position: absolute;top: 0rpx;right: 0px;"></uni-icons>
</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="subTitle" 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="4" :showBorder="false" :square="false">
<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>
</template>
脚本部分
接下来是组件的脚本部分:
<script>
export default {
name: 'my-audio',
emits: ['audioPlay', 'audioPause', 'audioEnd', 'audioCanplay', 'change', 'audioError'],
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
},
isShareBtn: {
type: Boolean,
default: false
},
},
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',
stringObject: (data) => {
return typeof(data)
},
innerAudioContext: uni.createInnerAudioContext()
}
},
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 H5
var 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 H5
this.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;
this.totalTimeText = this.getFormateTime(duration);
this.maxSliderIndex = parseFloat(duration).toFixed(2);
setTimeout(() => {
duration = this.innerAudioContext.duration;
this.totalTimeText = this.getFormateTime(duration);
this.maxSliderIndex = parseFloat(duration).toFixed(2);
}, 300)
});
this.innerAudioContext.onTimeUpdate((res) => {
this.sliderIndex = parseFloat(this.innerAudioContext.currentTime).toFixed(2);
this.currentTimeText = this.getFormateTime(this.innerAudioContext.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: {
audioDestroy() {
console.log("audioDestroy")
if (this.innerAudioContext) {
if (this.isPlaying && !this.innerAudioContext.paused) {
this.audioPause();
}
this.innerAudioContext.destroy();
this.isPlaying = false;
}
},
handleChangeAudioState() {
if (this.isPlaying && !this.innerAudioContext.paused) {
this.audioPause();
} else {
this.audioPlay();
}
},
audioPlay() {
this.innerAudioContext.play();
this.isPlaying = true;
},
audioPause() {
this.innerAudioContext.pause();
this.isPlaying = false;
},
handleSliderChange(e) {
this.changePlayProgress(e.detail ? e.detail.value : e)
},
handleChageSpeed() {
let speedCount = this.speedValue.length;
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();
},
handleFastRewind() {
if (this.IsReadyPlay) {
let value = parseInt(this.sliderIndex) - 15;
this.changePlayProgress(value >= 0 ? value : 0);
}
},
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);
},
getFormateTime(time) {
let ms = time * 1000;
let date = new Date(ms);
let hour = date.getUTCHours();
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');
},
},
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;
}
}
</style>
功能说明
播放控制
组件提供了播放、暂停、快进、快退、循环播放等功能。通过innerAudioContext
对象来控制音频的播放状态。
进度控制
通过slider
组件来控制音频的播放进度,用户可以拖动滑块来调整播放位置。
时间显示
组件会实时显示当前播放时间和总时间,通过getFormateTime
方法将秒数转换为00:00:00
格式。
事件处理
组件通过emits
属性定义了一系列事件,如播放、暂停、播放结束等,方便父组件监听和处理这些事件。
如何使用
将上述各个模块封装成my-audio.vue组件模块,放置在项目目录下的components目录下。然后可以新建一页面,在页面父组件中使用啦。如下:
playsong.vue
<template>
<view>
<view class="content">
<view>
<image class="cover" :src="cover" mode="aspectFill"></image>
</view>
<view class="myaudio">
<my-audio ref="audio" :src="vsrc" :title="song" :subTitle="sing"></my-audio>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
vsrc:'',
song:'',
sing:'',
cover:'',
}
},
onLoad(params) {
console.log('playsong onLoad');
console.log(params);
this.vsrc = params.url;
this.song = params.song;
this.sing = params.sing;
this.cover = params.cover;
},
onUnload() {
console.log('playsong onUnload');
this.$refs.audio.audioDestroy();
},
mounted() {
console.log("mounted")
},
methods: {
}
}
</script>
<style>
page {
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: #f8f4f5;
min-height: 100%;
height: auto;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx;
}
.cover{
border-radius: 10rpx;
}
.myaudio{
width: 100%;
}
</style>
注意,自定义组件内的声明周期函数没有多大作用,实测不生效的,因为这些是uni-app页面的生命周期函数,非组件的声明周期。在退出页面时,为了能够停止音乐播放,需要通过ref引用去调用组件的audioDestroy()函数。
onUnload() {
console.log('playsong onUnload');
this.$refs.audio.audioDestroy();
},
总结
通过上述代码封装,我们实现了一个功能齐全的自定义音乐播放器组件。这个组件不仅提供了基本的播放控制功能,还支持进度控制、时间显示和事件处理。希望这篇文章能帮助你理解如何使用UniApp框架封装自定义组件,并在实际项目中应用。
项目开源地址:
uni-app 影视类小程序开发从零到一 | 开源项目分享_uniapp 开源项目-CSDN博客
APP体验地址:关注微信公众号【毛青年】,回复“影视”获取。