使用UniApp实现自定义音乐播放器组件封装(简单轻量级,完美支持H5/小程序/Android)

在移动应用开发中,音乐播放器是一个常见的需求。本文将介绍如何使用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框架封装自定义组件,并在实际项目中应用。

项目开源地址:

imovie: 爱电影小程序uni-app

uni-app 影视类小程序开发从零到一 | 开源项目分享_uniapp 开源项目-CSDN博客

APP体验地址:关注微信公众号【毛青年】,回复“影视”获取。

其他资源

slider | uni-app官网

audio | uni-app官网

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值