图可能是这个样子, 记不清了
由于 audio 标签被说实在有点丑, 只能自己开始自定义, 找了很多资料, 自己后期加工,下面开始贴代码;
一. HTML代码片段(基于Angular9,css 文件里面没有见过的class都是自定义部分, 顾名思义即可)
<div class="audio-wrapper flex-start-center white margin-B10">
<audio class="margin-T15 audio-box" id="audio" src="{{音频文件路径}}"></audio> <!-- controls -->
<div class="audio-left font20 flex-center-center margin-R10 cursor" id="audioPlayer" (click)="play()">
<span *ngIf="player === 'play'" class="color-topic"><i nz-icon nzType="pause-circle" nzTheme="outline"></i></span> <!-- 播放 -->
<span *ngIf="player === 'pause'"><i nz-icon nzType="play-circle" nzTheme="outline"></i></span> <!-- 停止 -->
</div>
<!-- 开始时间 -->
<div class="audio-time margin-R15"><span class="audio-length-current" id="audioCurTime">00:00</span></div>
<!-- 进度条 -->
<div class="progress-bar-bg {{宽度200px}} relative" id="progressBarBg">
<div class="">
<span [ngClass]="currentTime != 0 ? '' : 'hide'" id="progressDot"></span>
<div class="progress-bar" id="progressBar"></div>
</div>
</div>
<!-- 结束时间 -->
<div class="audio-time {{左边距15px}}"><span class="audio-length-total" id="audioTotalTime">00:00</span></div>
<!-- 音量 -->
<div class="audio-volume margin-L10 cursor relative" (mouseover)="mouseVolume('mouseover')">
<span *ngIf="volumer === 'nomute'" (click)="volume($event)"><fa-icon [icon]="['fas', 'volume-up']"></fa-icon></span>
<span *ngIf="volumer === 'mute'" (click)="volume($event)"><fa-icon [icon]="['fas', 'volume-mute']"></fa-icon></span>
<div class="audio-volume-slider white absolute flex-justify-center" id="volumeSlider">
<div class="relative height100 flex-direction-column-reverse" id="volumeSliderStrip">
<span id="volumeDot white" class="absolute" id="volumeDot"></span>
<div class="volume-bar" id="volumeBar"></div>
</div>
</div>
</div>
<!-- 更多(下载) -->
<div class="audio-volume margin-L10 cursor" (click)="down()">
<span><i nz-icon nzType="more" nzTheme="outline"></i></span>
</div>
</div>
二, css 代码片段
audio{outline: none;} // 去掉默认标签外面的黑边框
.audio-wrapper{
width: 347px;
height: 48px;
padding: 0 31px;
.audio-left{
width: 24px;
height: 24px;
}
.audio-time{
width: 37px;
height: 14px;
}
.progress-bar-bg {
width: 142px;
height: 20px;
cursor: pointer;
padding: 9px 0;
&>div{
background-color: #D9DADC;
}
& span {
content: " ";
width: 10px;
height: 10px;
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
background-color: #fff;
position: absolute;
left: 0;
top: 50%;
margin-top: -5px;
margin-left: -5px;
cursor: pointer;
border: 2px solid red;
}
.progress-bar {
background-color: red;
width: 0;
height: 2px;
}
}
@mixin width-minXin {
width: 16px;
height: 16px;
}
/* 音量 */
.audio-volume{
@include width-minXin;
.audio-volume-slider-show{
animation: move-the-object 0.3s;
}
.audio-volume-slider{
width: 30px;
height: 100px;
cursor: pointer;
padding: 12px 0 5px 0;
bottom: 31px;
right: -2px;
box-shadow: 0 2px 12px 0;
display: none;
&>div{
background-color: rgba(0,0,0,0.04);
width: 4px;
&::after{
content: " ";
border-left: 6px solid transparent;
border-right: 7px solid transparent;
border-top: 8px solid #FFFFFF;
top: 87px;
position: absolute;
left: -4px;
}
}
& span {
content: " ";
width: 10px;
height: 10px;
border-radius: 50%;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
background-color: #fff;
position: absolute;
left: -3px;
top: 5%;
margin-top: -5px;
cursor: pointer;
border: 3px solid red;
}
.volume-bar {
background-color: red;
width: 4px;
height: 100%;
}
}
}
.audio-volume{
@include width-minXin;
}
}
三, 主要功能部分
1. 在页面渲染完成的方法中获取audio元素, 便于以后使用
this.audio = document.getElementById('audio');
// 监听进度条
fromEvent(this.audio, 'timeupdate').subscribe( () => {
this.updateProgress(this.audio);
});
// 监听播放完成事件
fromEvent(this.audio, 'ended').subscribe( () => {
this.audioEnded();
});
/* 音频进度条 */
// 点击进度条跳到指定点播放
// 注意:此处不要用click,否则下面的拖动进度点事件有可能在此处触发,此时e.offsetX的值非常小,会导致进度条弹回开始处(简直不能忍!!)
let progressBarBg = document.getElementById('progressBarBg');
let progressDot = document.getElementById('progressDot');
fromEvent(progressBarBg, 'mousedown').subscribe( (event) => {
this.progressBarBgMousedown = true;
let pgsWidth = parseFloat(window.getComputedStyle(progressBarBg, null).width.replace('px', ''));
let rate = event['offsetX'] / pgsWidth;
this.audio.currentTime = this.audio.duration * rate;
this.updateProgress(this.audio);
});
// 拖动进度条
this.dragProgressDotEvent(this.audio, progressDot, progressBarBg);
/* 音量调节器进度条 */
// 移入页面其他区域, 关闭音量调节器
let volumeSlider = document.getElementById('volumeSlider');
fromEvent(window, 'mouseout').subscribe( (e) => {
let volumeSliderStrip = document.getElementById('volumeSliderStrip');
if (!volumeSliderStrip) { // 不存在当前节点则不处理
return false;
}
let childNode = volumeSliderStrip.childNodes;
if (this.mouseoverAfter && volumeSlider !== e.target && volumeSliderStrip !== e.target && childNode[0] != e.target && childNode[1] != e.target) {
volumeSlider.style.display = 'none';
}
return false;
});
// 调节音量
let volumeDot = document.getElementById('volumeDot');
fromEvent(volumeSlider, 'mousedown').subscribe( (event) => {
let vsHeight = parseFloat(window.getComputedStyle(volumeSlider, null).height.replace('px', ''));
// 计算所占百分比, 偏移量 - 内边距 = 当前音量柱所占偏移量
let rate: number;
const volumeSliderStrip = document.getElementById('volumeSliderStrip');
const volumeBar = document.getElementById('volumeBar');
const vssHeight = vsHeight - 12 - 5; // 整条进度条高度
const offsetY = event['offsetY'];
if (event.target === volumeSliderStrip) { // 鼠标触发有颜色进度条,(偏移量 + 当前进度条剩余部分高度) / 整条进度条高度
rate = offsetY / vssHeight;
} else if (event.target === volumeBar) { // 鼠标触发剩余进度条,偏移量 / 整条进度条高度
const vbHeight = parseFloat(window.getComputedStyle(volumeBar, null).height.replace('px', ''));
rate = (offsetY + (vssHeight - vbHeight)) / vssHeight;
} else {
rate = (offsetY - 12) / vssHeight;
}
// 超出区域的事件触发不做处理
if (rate > 0 && rate < 1) {
this.updateVolumeProgress(1-rate);
}
});
fromEvent(volumeSlider, 'mouseout').subscribe((e) => {
volumeSlider.style.display = 'none';
})
// 拖动音量进度条
this.dragVolumeDotEvent(volumeDot, volumeSlider);
// 显示总时长
this.appService.setTime(1, 500).subscribe( ret => {
this.currentTime = this.audio.currentTime;
document.getElementById('audioTotalTime').innerText = this.transTime(this.audio.duration);
this.cdr.detectChanges();
});
2. 调节音量相关方法
/**
* 更新音量进度条
* @param rate 当前音量大小
*/
updateVolumeProgress(rate) {
document.getElementById('volumeBar').style.height = rate * 100 + '%';
document.getElementById('volumeDot').style.top = (1-rate) * 100 + '%';
this.audio.volume = rate;
}
/**
* 拖动调节音量
* @param {*} dot
* @param {*} barBg
*/
dragVolumeDotEvent(dot, barBg) {
let position = {
oriOffestTop: 0, // 移动开始时进度条的点距离进度条的偏移值 oriOffestTop
oriY: 0, // 移动开始时的Y坐标 oriY
};
let flag = false; // 标记是否拖动开始
let down = (event) => {
flag = true;
position.oriOffestTop = dot.offsetTop;
position.oriY = event.touches ? event.touches[0].clientY : event.clientY; // 要同时适配mousedown和touchstart事件
// 禁止默认事件(避免鼠标拖拽进度点的时候选中文字)
if (event && event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
// 禁止事件冒泡
if (event && event.stopPropagation) {
event.stopPropagation();
} else {
window.event.cancelBubble = true;
}
}
let move = (event) => {
if (flag) {
let clientY = event.touches ? event.touches[0].clientY : event.clientY; // 要同时适配mousemove和touchmove事件
let length = clientY - position.oriY;
let vsHeight = parseFloat(window.getComputedStyle(barBg, null).height.replace('px', ''));
const vssHeight = vsHeight - 12 - 5; // 整条进度条高度
let rate = (position.oriOffestTop + length) / vssHeight;
// 超出区域的事件触发不做处理
if (rate > 0 && rate < 1) {
this.updateVolumeProgress(1-rate);
}
}
}
let end = () => {
flag = false;
}
// 鼠标按下时
fromEvent(dot, 'mousedown').subscribe(down);
fromEvent(dot, 'touchstart').subscribe(down);
// 开始拖动
fromEvent(document, 'mousemove').subscribe(move);
fromEvent(document, 'touchmove').subscribe(move);
// 拖动结束
fromEvent(document, 'mouseup').subscribe(end);
fromEvent(document, 'touchend').subscribe(end);
}
/**
* 音量打开关闭
*/
volume(event) { // nomute / 静音 mute
if (this.volumer === 'nomute') { // 静音
this.audio.muted = true;
} else { // 打开声音
this.audio.muted = false;
}
this.volumer = this.volumer === 'nomute' ? 'mute' : 'nomute';
event.stopPropagation();
}
/**
* 鼠标移入显示音量调节器
*/
mouseVolume(type) {
if (type === 'mouseover') { // 鼠标移入
this.mouseoverAfter = false;
document.getElementById('volumeSlider').style.display = 'flex';
// 四秒如果还没有移到目标dom, 则关闭音量调节框
this.appService.setTime(1, 4000).subscribe( ret => {
this.mouseoverAfter = true;
});
}
}
3. 播放器相关方法
/**
* 播放
*/
play() {
if (this.player === 'pause') { // 播放
this.audio.play();
} else {
this.audio.pause(); // 停止
}
this.player = this.player === 'pause' ? 'play' : 'pause';
}
/**
* 鼠标拖动进度点时可以调节进度
* @param {*} audio
* @param {*} dot
* @param {*} barBg
*/
dragProgressDotEvent(audio, dot, barBg) {
let position = {
oriOffestLeft: 0, // 移动开始时进度条的点距离进度条的偏移值
oriX: 0, // 移动开始时的x坐标
maxLeft: 0, // 向左最大可拖动距离
maxRight: 0 // 向右最大可拖动距离
};
let flag = false; // 标记是否拖动开始
let down = (event) => {
if (!audio.paused || audio.currentTime != 0) { // 只有音乐开始播放后才可以调节,已经播放过但暂停了的也可以
flag = true;
position.oriOffestLeft = dot.offsetLeft;
position.oriX = event.touches ? event.touches[0].clientX : event.clientX; // 要同时适配mousedown和touchstart事件
position.maxLeft = position.oriOffestLeft; // 向左最大可拖动距离
position.maxRight = barBg.offsetWidth - position.oriOffestLeft; // 向右最大可拖动距离
// 禁止默认事件(避免鼠标拖拽进度点的时候选中文字)
if (event && event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
// 禁止事件冒泡
if (event && event.stopPropagation) {
event.stopPropagation();
} else {
window.event.cancelBubble = true;
}
}
}
let move = (event) => {
if (flag) {
var clientX = event.touches ? event.touches[0].clientX : event.clientX; // 要同时适配mousemove和touchmove事件
var length = clientX - position.oriX;
if (length > position.maxRight) {
length = position.maxRight;
} else if (length < -position.maxLeft) {
length = -position.maxLeft;
}
let pgsWidth = parseFloat(window.getComputedStyle(barBg, null).width.replace('px', ''));
let rate = (position.oriOffestLeft + length) / pgsWidth;
audio.currentTime = audio.duration * rate;
this.updateProgress(audio);
}
}
let end = () => {
flag = false;
}
// 鼠标按下时
fromEvent(dot, 'mousedown').subscribe(down);
fromEvent(dot, 'touchstart').subscribe(down);
// 开始拖动
fromEvent(document, 'mousemove').subscribe(move);
fromEvent(document, 'touchmove').subscribe(move);
// 拖动结束
fromEvent(document, 'mouseup').subscribe(end);
fromEvent(document, 'touchend').subscribe(end);
}
/**
* 更新进度条与当前播放时间
* @param {object} audio - audio对象
*/
updateProgress(audio) {
this.currentTime = audio.currentTime; // 赋值当前时间
let value = audio.currentTime / audio.duration;
document.getElementById('progressBar').style.width = value * 100 + '%';
document.getElementById('progressDot').style.left = value * 100 + '%';
document.getElementById('audioCurTime').innerText = this.transTime(audio.currentTime);
this.cdr.detectChanges();
}
/**
* 播放完成时把进度调回开始的位置
*/
audioEnded() {
this.player = 'pause'; // 播放完成之后回到初始状态
document.getElementById('progressBar').style.width = 0 + 'px';
document.getElementById('progressDot').style.left = 0 + 'px';
document.getElementById('audioCurTime').innerText = this.transTime(0);
this.currentTime = 0; // 赋值当前时间
this.cdr.detectChanges();
}
/**
* 音频播放时间换算
* @param value - 音频当前播放时间,单位秒
*/
transTime(value) {
let time = "";
let h = parseInt((value / 3600).toString());
value %= 3600;
let m = parseInt((value / 60).toString());
let s = parseInt((value % 60).toString());
if (h > 0) {
time = this.formatTime(h + ":" + m + ":" + s);
} else {
time = this.formatTime(m + ":" + s);
}
return time;
}
/**
* 格式化时间显示,补零对齐
* eg:2:4 --> 02:04
* @param {string} value - 形如 h:m:s 的字符串
*/
formatTime(value) {
let time = "";
let s = value.split(':');
let i = 0;
for (; i < s.length - 1; i++) {
time += s[i].length == 1 ? ("0" + s[i]) : s[i];
time += ":";
}
time += s[i].length == 1 ? ("0" + s[i]) : s[i];
return time;
}