该功能将使用vue3 + TS来实现语音播放组件,使用什么技术不重要,重要的是看懂了核心逻辑后,通过原生js、react、vue2等都可以轻松实现

所涉及到重要点有以下几个:

(1)进度条的实现:拖拽进度条、点击进度条

(2)操作audio语音播放:通过js操作audio媒体

(3)播放进度与进度条紧密关联:播放的进度改变时,进度条也随之改变;进度条改变时,播放的进度也随之改变

效果图:

开始我们的设计吧!

第一步:点击拖拽进度条

进度条的css样式如下:

父元素设置灰色背景色,圆圈进行position定位,使用left百分比,同时黑色进度条的width也是百分比,这样圆圈的left值是多少,黑色进度条的width就是多少。

.slider-wrap {position: relative;display: flex;align-items: center;height: 4px;max-width: 194px;min-width: 36px;width: 194px;background-color: rgba(23, 23, 23, 0.15);cursor: pointer;.circle {position: absolute;width: 14px;height: 14px;background-color: #555555;border-radius: 100%;cursor: pointer;user-select: none;transform: translate(-50%);}.slider-bar {height: 4px;max-width: 200px;background-color: #555555;}}

先说拖拽圆圈,圆圈上绑定mousedown事件,根据事件e.target确定圆圈、黑色进度条、灰色父元素,三者的element。同时知道了圆圈当前的left值,比如30%,还知道了当前鼠标mousedown时,事件e.pageX,即鼠标mousedown时,距离页面左边的水平值,因为对比后续鼠标移动时,触发的mousemove事件的e.pageX可以判断移动了多少。同时还要知道灰色背景的父元素的width。因为鼠标移动的距离 / width 要赋值给圆圈的left。知道移动了%多少。

    const circleMousedown = (e) => {circleTarget.el = e.target; // 圆圈自身wrapEle.el = e.target.parentElement; // 圆圈父元素sliderBar.el = e.target.nextElementSibling; // 圆圈的兄弟节点circleTarget.circleLeft = e.target.style.left;circleTarget.pageX = e.pageX;circleTarget.circleMouseMouve = true;wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');};

然后,监听document文档的mousemove,注意鼠标是可以在整个文档上移动的,不过圆圈可不能在灰色父元素之外。移动的e.pageX - 鼠标mousedown时的e.pageX 就是鼠标水平移动的距离。超出最大值时,圆圈left设置为100%,小于最小值时,left设置为0%,总之left要在0%~100%之间,才能保证圆圈不超出到外面去。这样圆圈就可以随着鼠标移动了,同时黑色进度条的width值与圆圈的left值一样,所以黑色进度条的width也随着鼠标移动。

document.addEventListener('mousemove', (e) => {if (circleTarget.circleMouseMouve) {const nowLeft =parseFloat(circleTarget.circleLeft) +getPercentage(e.pageX - circleTarget.pageX, wrapEle.width);if (nowLeft >= 100) {circleDragLeft = '100%';} else if (nowLeft <= 0) {circleDragLeft = '0%';} else {circleDragLeft = `${nowLeft}%`;}updateProgressBar(circleDragLeft);currentTimeByProgressBar(circleDragLeft);}});document.addEventListener('mouseup', () => {circleTarget.circleMouseMouve = false;});

再说说点击父元素时,圆圈到指定位置

点击事件在灰色父元素上进行监听,注意e.target可不一定是灰色父元素,e.target表示鼠标点击到哪个元素,随后click冒泡到父元素上的。同样点击事件的e.pageX 可以确定鼠标点击的水平位置,转换为%值,设置圆圈的left值和黑色进度条的width值。

    // 只处理e.target是slider-wrap 或 slider-bar的情况const clickSliderWrap = (e) => {if (e.target.getAttribute('target') === 'wrap') {wrapEle.el = e.target;circleTarget.el = e.target.firstElementChild;sliderBar.el = e.target.lastElementChild;} else if (e.target.getAttribute('target') === 'sliderbar') {sliderBar.el = e.target;circleTarget.el = e.target.previousElementSibling;wrapEle.el = e.target.parentElement;} else {return;}wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');const styleLeft = `${getPercentage(e.offsetX, wrapEle.width)}%`;updateProgressBar(styleLeft);currentTimeByProgressBar(styleLeft);};

以上就可以实现进度条功能了。

第二步:操作媒体音频

获取audio的element,audioElement上面有play、pause等方法,还有currentTime播放进度时间,以及duration总时长。所以说HTML5的audio标签,上面的方法和属性还是非常直观的,这也正是web发展的一个特点,某个新的特性的产生,功能会很明了。

首先当媒体的第一帧加载完成时,我们就获取audio的element:(audio自身的loadeddata事件)

// 当媒体音频第一帧加载完成时const audioLoadeddata = (e) => {audioEl = e.target;audioData.duration = e.target.duration;};

其次,对播放中进行监听:(audio的timeupdate事件)

    // 播放进度:表示audio正在播放,currentTime在更新const audioTimeupdate = (e) => {audioData.currentTime = e.target.currentTime;progressBarBycurrentTime();};

最后,播放完成进行监听:(audio的ended事件)

    // 音频播放结束const audioEnded = () => {audioData.playing = false;};

如果对audio标签不是很熟悉,请参考文档:<audio>: The Embed Audio element - HTML: HyperText Markup Language | MDN

上述操作还是很简单的,audio上的属性、方法、事件都是非常简单明了且实用的。

第三步:进度条和播放进度关联

通过audio当前的播放时间 / 总时长,得到的%值,赋值给圆圈的left和黑色进度条的width。

通过圆圈的left值的% * 总时长,得到audio的当前播放时间。(audio的currentTime属性直接赋值,语音播放就会跳转到指定的时间进行播放,比如 1,就会从1秒的位置开始)

以下是完整代码:

<template><div class="glowe-audio"><div class="audio"><div class="icon-div" @click="playPauseAudio"><video-play class="icon" v-if="!audioData.playing"></video-play><video-pause class="icon" v-else></video-pause></div><divclass="slider-wrap":style="{ width: durationToWidth(audioData.duration) }"target="wrap"@click="clickSliderWrap"><div class="circle" target="circle" style="left: 0%" @mousedown="circleMousedown"></div><div class="slider-bar" target="sliderbar" style="width: 0%"></div></div><div class="time-wrap"><span class="time">{{ durationFormat(Math.round(audioData.duration)) }}</span></div></div><audio:src="audioData.audiourl"preload="auto"@ended="audioEnded"@timeupdate="audioTimeupdate"@loadeddata="audioLoadeddata"></audio></div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { VideoPause, VideoPlay } from '@element-plus/icons';
import { durationToFormat } from '@/utils/refactor';export default defineComponent({name: 'GloweAudio',components: {VideoPlay,VideoPause,},props: {audioUrl: {type: String,required: true,},},setup(props) {const audioData = reactive({audiourl: props.audioUrl,playing: false,duration: 0, // 音频总时长currentTime: 0, // 当前播放的位置});let audioEl: HTMLAudioElement | null = null;const wrapEle: {width: string;el: any;} = {width: '0px',el: null,};const sliderBar: {width: string;el: any;} = {width: '0%',el: null,};const circleTarget: {circleMouseMouve: boolean;pageX: number;circleLeft: string;el: any;} = {circleMouseMouve: false,pageX: 0,circleLeft: '0%',el: null,};let circleDragLeft = '0%'; // 圆圈被鼠标水平拖拽的距离(默认向左)document.addEventListener('mousemove', (e) => {if (circleTarget.circleMouseMouve) {const nowLeft =parseFloat(circleTarget.circleLeft) +getPercentage(e.pageX - circleTarget.pageX, wrapEle.width);if (nowLeft >= 100) {circleDragLeft = '100%';} else if (nowLeft <= 0) {circleDragLeft = '0%';} else {circleDragLeft = `${nowLeft}%`;}updateProgressBar(circleDragLeft);currentTimeByProgressBar(circleDragLeft);}});document.addEventListener('mouseup', () => {circleTarget.circleMouseMouve = false;});const circleMousedown = (e) => {circleTarget.el = e.target; // 圆圈自身wrapEle.el = e.target.parentElement; // 圆圈父元素sliderBar.el = e.target.nextElementSibling; // 圆圈的兄弟节点circleTarget.circleLeft = e.target.style.left;circleTarget.pageX = e.pageX;circleTarget.circleMouseMouve = true;wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');};// 只处理e.target是slider-wrap 或 slider-bar的情况const clickSliderWrap = (e) => {if (e.target.getAttribute('target') === 'wrap') {wrapEle.el = e.target;circleTarget.el = e.target.firstElementChild;sliderBar.el = e.target.lastElementChild;} else if (e.target.getAttribute('target') === 'sliderbar') {sliderBar.el = e.target;circleTarget.el = e.target.previousElementSibling;wrapEle.el = e.target.parentElement;} else {return;}wrapEle.width = window.getComputedStyle(wrapEle.el, null).getPropertyValue('width');const styleLeft = `${getPercentage(e.offsetX, wrapEle.width)}%`;updateProgressBar(styleLeft);currentTimeByProgressBar(styleLeft);};// 播放或暂停音频const playPauseAudio = (e) => {const iconDiv = findParentsEl(e.target.parentElement, 'icon-div');wrapEle.el = iconDiv?.nextElementSibling;circleTarget.el = wrapEle.el.firstElementChild;sliderBar.el = wrapEle.el.lastElementChild;const parentAudio = findParentsEl(e.target.parentElement, 'audio');if (parentAudio) {if (!audioData.playing) {audioPlay();} else {audioPause();}}};// 计算百分比的分子function getPercentage(num: number | string, den: number | string): number {const numerator = typeof num === 'number' ? num : parseFloat(num);const denominator = typeof den === 'number' ? den : parseFloat(den);return Math.round((numerator / denominator) * 10000) / 100;}// 查找自身或最近的一个父元素有className的function findParentsEl(el: HTMLElement, classname: string): HTMLElement | null {// 注意avg className得到一个对象而非字符串if (el && el.className?.split && el.className.split(' ').includes(classname)) {return el;}if (el.parentElement) {if (el.parentElement.className.split(' ').includes(classname)) {return el.parentElement;} else {return findParentsEl(el.parentElement, classname);}}return null;}/*** 更新进度条* @param percentage 得到一个百分比的字符串*/function updateProgressBar(percentage: string) {circleTarget.el.style.left = percentage;sliderBar.el.style.width = percentage;}/*** 以下是对音频的操作*/// 音频播放结束const audioEnded = () => {audioData.playing = false;};// 播放进度:表示audio正在播放,currentTime在更新const audioTimeupdate = (e) => {audioData.currentTime = e.target.currentTime;progressBarBycurrentTime();};// 当媒体音频第一帧加载完成时const audioLoadeddata = (e) => {audioEl = e.target;audioData.duration = e.target.duration;};// 播放function audioPlay() {if (audioEl) {audioEl.play();audioData.playing = true;}}// 暂停播放function audioPause() {if (audioEl) {audioEl.pause();audioData.playing = false;}}// 进度条和音频播放进度进行关联function progressBarBycurrentTime() {const progress = getPercentage(audioData.currentTime, audioData.duration);updateProgressBar(`${progress}%`);}/*** 播放进度与进度条进行关联* @param stylePercentage 圆圈的left值*/function currentTimeByProgressBar(styleLeft: string) {if (audioEl) {const currentTime = (parseFloat(styleLeft) / 100) * audioData.duration;audioEl.currentTime = currentTime;audioData.currentTime = currentTime;}}const durationFormat = (num: number): string => {return durationToFormat(num, 'm:ss');};const durationToWidth = (num: number): string => {return `${Math.ceil((158 / 58) * num + 33)}px`;};return {audioData,circleMousedown,clickSliderWrap,playPauseAudio,audioEnded,audioTimeupdate,audioLoadeddata,durationFormat,durationToWidth,};},
});
</script>
<style scoped lang="scss">
.glowe-audio {.audio {display: flex;justify-content: space-evenly;align-items: center;max-width: 308px;height: 48px;.icon-div {width: 20px;height: 20px;border-radius: 100%;margin-left: 22px;margin-right: 17px;.icon {cursor: pointer;}}.slider-wrap {position: relative;display: flex;align-items: center;height: 4px;max-width: 194px;min-width: 36px;width: 194px;background-color: rgba(23, 23, 23, 0.15);cursor: pointer;.circle {position: absolute;width: 14px;height: 14px;background-color: #555555;border-radius: 100%;cursor: pointer;user-select: none;transform: translate(-50%);}.slider-bar {height: 4px;max-width: 200px;background-color: #555555;}}.time-wrap {margin-left: 15px;margin-right: 18px;.time {font-size: 12px;}}}
}
</style>

audio语音播放组件相关推荐

  1. html audio语音播放器,HTML5-定制音频播放器-audio

    先看看效果--->传送门<---,如果感觉没什么大不了的就可以绕道了(==) HTML结构其实很简单,不要在意那个音频的地址. 首先是audio标签,是我们这个小东西的核心.其主要的属性可 ...

  2. audio语音播放,在ios上失效

    currentTime 属性设置失效 当需要拖动滑块控制音频时,先设置音频只有在播放的状态下才能拖动重新设置currentTime. 代码片段: onDrag(value, index, ext) { ...

  3. uni-app 实现语音播放实现思路和代码

    #uni-app 实现语音播放功能demo == 思路 1== 从消息列表中筛选出单独的语音列表, 在消息列表中添加字段,语音列表的index和消息列表中的index对应 语音列表添加标识,区分播放状 ...

  4. js模仿微信语音播放的小功能

    自己写的一个模仿微信语音播放的小功能,实现的主要功能是:点击播放,点击暂停,播放切换,,,  代码如下: <!DOCTYPE html> <html lang="en&qu ...

  5. php仿微信语音条,html5的audio实现高仿微信语音播放效果

    前言 之前做过一个微信的项目,专家回复可以录音,然后储存成mp3格式,前台可以获取mp3,客户可以在线试听mp3录音效果,今天就简单分享一下这个效果如何实现,及实现思路和方法! 效果图 前台大体呈现效 ...

  6. html5的audio实现高仿微信语音播放效果

    效果图 前台大体呈现效果图如下: 点击就可以播放mp3格式的录音.点击另外一个录音,当前录音停止! 思路 关于播放动画,这个很简单,我们可以用css3的逐帧动画来实现.关于逐帧动画,我之前的文章也写过 ...

  7. animation基础练习源码_用vue简单写一个音乐播放组件「附源码」

    作者:vipbic 转发链接:https://segmentfault.com/a/1190000022980992 前言 上次小编也分享一个关于Vue 开发过音乐播放对项目: 基于 electron ...

  8. c语言微信小程序编程,微信小程序实现类似微信点击语音播放效果

    本文实例为大家分享了微信小程序类似平常微信语音聊天的效果,不会互相干扰播放状态,供大家参考,具体内容如下 根据开发的需求,先理清一下思路,点击语音播放与暂停/停止,切换下一个语音等: 小程序对于aud ...

  9. 微信小程序仿微信聊天语音播放自定义控件

    效果如↓↓↓        假装有声音. 很郁闷,没有做到完全解耦,试了试音频播放组件<audio></audio>与API wx.createInnerAudioContex ...

  10. 微信小程序,类似微信点击语音播放效果,不会互相干扰播放状态

    类似平常微信语音聊天的效果- 根据开发的需求,先理清一下思路,点击语音播放与暂停/停止,切换下一个语音等: 小程序对于audio的组件的一些api方法已经不支持了,详情可以参看:微信小程序audio组 ...

最新文章

  1. php图片编辑失真,PHP上传真彩图片缩略图质量失真解决方法
  2. SAP RETAIL WRMO 补货监控
  3. String类中的equals方法与Object类中的equals方法的不同点
  4. html5游戏制作入门系列教程(七)
  5. LeetCode 155. Min Stack
  6. Intel Sandy Bridge/Ivy Bridge架构/微架构/流水线 (19) - 系统代理
  7. sqlite3学习笔记-方法介绍和测试代码
  8. asp.net 利用DirectoryEntry来验证用户以及开机密码
  9. Computer:路由器连接交换机怎么建立局域网
  10. 5--残差网络(ResNet)
  11. DAS,NAS,SAN 三种存储技术比较
  12. runtime error
  13. Java——IDEA
  14. 2022.5.4.学习笔记数据类型
  15. MPU9250的基本框架
  16. Uinux/linux vi保存退出命令 (如何退出vi)
  17. 小飞鱼通达二开 解决通达OA数据库服务不能启动的问题(图文)
  18. Vue3状态管理库——Pinia
  19. 夏令营,预推免经验分享(计科方向,天大+北理+上交+国防科大+北邮+清华深圳)
  20. android 接听和挂断实现方式

热门文章

  1. 清理C盘——这个操作让你的C盘多出20G空间
  2. 很遗憾,该服务器不支持 jmail 组件!,Jmail组件安装方法及Windows 7系统下Jmail组件注册失败解决方法...
  3. 电子发票专用的邮箱—理票侠
  4. 【稀饭】react native 系列教程之已有项目接入React Native
  5. linux源码编译ipk,OpenWrt-SDK-编译生成ipk软件包
  6. UE4C++吃豆子游戏
  7. 仿城通网盘下载页面源码
  8. 计算机无法启动printspooler,Win7系统下print spooler服务无法启动的解决方法
  9. ChromeOS+Win双系统安装教程
  10. 怎样在html中写css样式,css样式应该怎么写?