VUE实现模拟播放器

845 阅读3分钟

VUE实现模拟播放器

作者:岩雷

项目背景

默认音乐播放器在不同的浏览器有不同的展现形式,且UI过于单一,不能满足页面整体UI一致性,从而不能满足业务的需求。

效果展示

1616744993(1).jpg

audio基础知识

属性:
autoplay:音频自动播放;
controls:提供一个包含声音,播放进度,播放暂停的控制面板,让用户可以控制音频的播放;
currentTime:当前播放时间;
duration:音频总长度;
loop: 循环播放;
muted: 静音;
src:音频的URL;
方法:
play: 播放;
pause: 暂停;
timeupdate:当currentTime更新时会触发timeupdate事件

实现思路

1、点击按钮实现播放与暂停
2、默认播放实时改变进度条的宽度和滑动图标的位置
3、点击进度条改变音频的进度
4、划动进度条改变音频的进度
PS:进度条点击和滑动进度图标增加点击范围

实现流程

概念名词:

  • 默认滑动条
  • 进度滑动条
  • 滑动进度图标
不添加controls,默认的播放器会隐藏
<audio
    src="./aa.mp3"
    controls="controls"
    loop
    class="audio1"
    @timeupdate="updateTime"
    ref="audio"
></audio>

实现模拟音乐播放器,主要需要监听timeupdate方法,currentTime当前的播放进度实时更新,从而实时改变进度条和滑动图标的位置。

1、点击按钮实现播放与暂停

data中添加是否播放参数isPlay(默认false),通过监听isPlay的变化计算显示播放按钮还是暂停按钮
computed: {
    // 播放按钮显示
    isPlayOrPause() {
        return this.isPlay ? "pause-circle-o" : "play-circle-o";
    }
    // 播放进度比例,总长度也课通过duration获取
    percent() {
        return this.currentTime / this.totalTime;
    },
}

2、监听timeupdate方法,存储当前的播放进度

updateTime(e) {
    this.currentTime = e.target.currentTime;
}

3、默认播放,监听percent变化,改变进度条和滑动图标的位置

watch: {
    percent(newPrecent) {
        // 非滑动进度图标时间内
        if (newPrecent >= 0&& !this.touch.initiated) {
            // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
            const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
            // 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
            const offsetWidth = newPrecent * barWidth
            // 进度滑动条偏移和滑动进度图标偏移
            this._offset(offsetWidth)
        }
    }
}

4、点击默认滑动条,更改音频播放进度

progressClick(e) {
    // getBoundingClientRect用于获取某个元素相对于视窗的位置集合
    const rect = this.$refs.progressBar.getBoundingClientRect();
    // pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
    const offsetWidth = e.pageX - rect.left;
    // 进度滑动条偏移和滑动进度图标偏移
    this._offset(offsetWidth);
    // 计算当前播放时间
    this._triggerPercent();
},
// 计算当前时间
_triggerPercent() {
    // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
    const barWidth =
        this.$refs.progressBar.clientWidth - progressBtnWidth;
    // 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
    const percent = this.$refs.progress.clientWidth / barWidth;
    // 当前播放时长 = 总时长 * 播放比例
    const currentTime = this.totalTime * percent;
    // 设置音频当前播放时间
    this.$refs.audio.currentTime = currentTime;
},
// 计算拖动条的宽度和滑动偏移的距离
_offset(offsetWidth) {
    this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
    this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
},

5、滑动进度图标,更改音频播放进度,分三个阶段:

  • 滑动开始(start):记录当前点距离页面左侧的距离和进度滑动条的宽度
  • 滑动中(move):实时记录进度滑动条的偏移量,从而计算进度滑动条和滑动进度图标的当前位置
  • 滑动结束(end):设置音频当前播放时间
progressTouchStart(e) {
    this.touch.initiated = true; // 标志位 表示初始化
    this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
    this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
    console.log("start", this.touch.startX);
},
progressTouchMove(e) {
    console.log("move");
    if (!this.touch.initiated) {
        return;
    }
    // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
    const barWidth =
        this.$refs.progressBar.clientWidth - progressBtnWidth;
    const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
    // 滑动中进度滑动条和滑动进度图标的当前位置
    const offsetWidth = Math.min(
        barWidth,
        Math.max(0, this.touch.left + deltaX)
    );
    this._offset(offsetWidth);
},
progressTouchEnd() {
    console.log("end");
    this.touch.initiated = false;
    this._triggerPercent();
},

最终代码

<template>
    <div class="audio-wrapper">
        <audio
            src="./aa.mp3"
            controls="controls"
            loop
            class="audio1"
            @timeupdate="updateTime"
            ref="audio"
        ></audio>
        <div class="fh-progress-wrapper">
            <span class="fh-time fh-time-l">{{ format(currentTime) }}</span>
            <div class="fh-progress-bar-wrapper">
                <div
                    class="fh-progress-bar"
                    ref="progressBar"
                    @click="progressClick"
                >
                    <div class="fh-bar-inner">
                        <div class="fh-progress" ref="progress"></div>
                        <div
                            class="fh-progress-btn-wrapper"
                            ref="progressBtn"
                            @touchstart.prevent="progressTouchStart"
                            @touchmove.prevent="progressTouchMove"
                            @touchend="progressTouchEnd"
                        >
                            <div class="fh-progress-btn"></div>
                        </div>
                    </div>
                </div>
            </div>
            <span class="fh-time fh-time-r">{{ format(totalTime) }}</span>
        </div>
        <div class="isPlayOrPause" @click="onHandle">
            <van-icon :name="isPlayOrPause" />
        </div>
    </div>
</template>
<script>
const progressBtnWidth = 16;
import { Icon } from "vant";
export default {
    name: "index",
    components: {
        [Icon.name]: Icon
    },
    data() {
        return {
            currentTime: 0, // 当前播放时间
            touch: {}, // 滑动图标时记录的参数
            isPlay: false, // 是否播放
            totalTime: 23
        };
    },
    computed: {
        percent() {
            return this.currentTime / this.totalTime;
        },
        isPlayOrPause() {
            return this.isPlay ? "pause-circle-o" : "play-circle-o";
        }
    },
    methods: {
        onHandle() {
            if (!this.isPlay) {
                this.$refs.audio.play();
            } else {
                this.$refs.audio.pause();
            }
            this.isPlay = !this.isPlay;
        },
        // 当currentTime更新时会触发timeupdate事件
        updateTime(e) {
            this.currentTime = e.target.currentTime;
            // console.log("currentTime " + this.currentTime);
        },
        progressTouchStart(e) {
            this.touch.initiated = true; // 标志位 表示初始化
            this.touch.startX = e.touches[0].pageX; // 当前拖动点X轴位置
            this.touch.left = this.$refs.progress.clientWidth; // 当前进度条位置
            console.log("start", this.touch.startX);
        },
        progressTouchMove(e) {
            console.log("move");
            if (!this.touch.initiated) {
                return;
            }
            // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
            const barWidth =
                this.$refs.progressBar.clientWidth - progressBtnWidth;
            const deltaX = e.touches[0].pageX - this.touch.startX; // 拖动偏移量
            // 滑动中进度滑动条和滑动进度图标的当前位置
            const offsetWidth = Math.min(
                barWidth,
                Math.max(0, this.touch.left + deltaX)
            );
            this._offset(offsetWidth);
        },
        progressTouchEnd() {
            console.log("end");
            this.touch.initiated = false;
            this._triggerPercent();
        },
        progressClick(e) {
            // getBoundingClientRect用于获取某个元素相对于视窗的位置集合
            const rect = this.$refs.progressBar.getBoundingClientRect();
            // pageX() 属性是鼠标指针的位置,相对于文档的左边缘。
            const offsetWidth = e.pageX - rect.left;
            // 进度滑动条偏移和滑动进度图标偏移
            this._offset(offsetWidth);
            // 计算当前播放时间
            this._triggerPercent();
        },
        // 计算当前时间
        _triggerPercent() {
            // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
            const barWidth =
                this.$refs.progressBar.clientWidth - progressBtnWidth;
            // 当前播放比例 = 进度滑动条宽度 / 默认滑动条实际宽度
            const percent = this.$refs.progress.clientWidth / barWidth;
            // 当前播放时长 = 总时长 * 播放比例
            const currentTime = this.totalTime * percent;
            // 设置音频当前播放时间
            this.$refs.audio.currentTime = currentTime;
        },
        // 计算拖动条的宽度和滑动偏移的距离
        _offset(offsetWidth) {
            this.$refs.progress.style.width = `${offsetWidth}px`; // 进度条偏移
            this.$refs.progressBtn.style.transform = `translate3d(${offsetWidth}px, 0, 0)`; // 小球偏移
        },
        format(interval) {
            interval = interval | 0; // 向下取整
            const minute = (interval / 60) | 0;
            const second = this._pad(interval % 60);
            return `${minute}:${second}`;
        },
        _pad(num, n = 2) {
            // 用0补位,补2位字符串长度
            let len = num.toString().length;
            while (len < n) {
                num = "0" + num;
                len++;
            }
            return num;
        }
    },

    watch: {
        percent(newPrecent) {
            // 非滑动进度图标时间内
            if (newPrecent >= 0 && !this.touch.initiated) {
                // 默认滑动条实际宽度 = 默认滑动条宽度 - 滑动进度图标宽度
                const barWidth =
                    this.$refs.progressBar.clientWidth - progressBtnWidth;
                // 进度滑动条宽度 = 默认滑动条实际宽度 * 播放百分比
                const offsetWidth = newPrecent * barWidth;
                // 进度滑动条偏移和滑动进度图标偏移
                this._offset(offsetWidth);
            }
        }
    }
};
</script>
<style lang="less" scoped>
.audio-wrapper {
    background-color: tomato;
}
.audio1 {
    margin: auto;
    display: block;
    width: 100%;
}
.fh-progress-wrapper {
    display: flex;
    align-items: center;
    width: 80%;
    margin: 0px auto;
    padding: 10px 0;

    .fh-time {
        color: #fff;
        font-size: 12px;
        flex: 0 0 30px;
        line-height: 30px;
        width: 30px;

        &.fh-time-l {
            text-align: left;
        }

        &.fh-time-r {
            text-align: right;
        }
    }

    .fh-progress-bar-wrapper {
        flex: 1;
    }
}
.fh-progress-bar {
    height: 30px;

    .fh-bar-inner {
        position: relative;
        top: 13px;
        height: 4px;
        background: rgba(0, 0, 0, 0.3);

        .fh-progress {
            position: absolute;
            height: 100%;
            background: #41b883;
        }

        .fh-progress-btn-wrapper {
            position: absolute;
            left: -7px;
            top: -13px;
            width: 30px;
            height: 30px;

            .fh-progress-btn {
                position: relative;
                top: 7px;
                left: 7px;
                box-sizing: border-box;
                width: 16px;
                height: 16px;
                border: 3px solid #fff;
                border-radius: 50%;
                background: #41b883;
            }
        }
    }
}
.isPlayOrPause {
    text-align: center;
    font-size: 60px;
}
</style>

技术分享宣传图@3x.png