自定义 audio 样式以及各种事件

本文档展示了如何使用Angular9构建一个自定义的音频播放器,包括播放/暂停按钮、进度条、音量控制和时间显示等功能。通过HTML和CSS代码实现了播放器的样式和交互,并通过事件监听处理播放、进度调整和音量控制等操作。
摘要由CSDN通过智能技术生成

图可能是这个样子, 记不清了

 

由于 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;
    }

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值