移动端自制音乐播放器--React

相信你们都有过开发音乐播放器的需求,我也不例外,最近恰好就作了一个这样的需求,因此就顺便把遇到的问题和踩过的坑,分享一下~css

由于项目是基于React,因此下面的源码基本都是在播放器组件中抽出来的,不过不管在什么框架下开发,逻辑是同样滴。html

播放器皮肤

先从简单的开始讲起吧,说到播放器几个经常使用的操做按钮确定少不了,可是最重要的是那一个能够拖动的播放条。浏览器

<!--进度条begin-->
<div className="audio-progress" ref={(r) => { this.audioProgress = r }}>
    <div className="audio-progress-bar" ref={(bar) => { this.progressBar = bar }}>
        <div className="audio-progress-bar-preload" style={{ width: bufferedPercent + '%' }}></div>
        <div className="audio-progress-bar-current" style={{ width: `calc(${currentTime}/${currentTotalTime} * ${this.progressBar && this.progressBar.clientWidth}px)` }}></div>
    </div>
    <div className="audio-progress-point-area" ref={(point) => { this.audioPoint = point }} style={{ left: left + 'px' }}>
        <div className="audio-progress-point">

        </div>
    </div>
</div>
<!--进度条begin-->
    <div className="audio-timer">
        <p>{this.renderPlayTime(currentTime)}</p>
        <p>{this.renderPlayTime(currentTotalTime)}</p>
    </div>

    <div className="audio-control">
        <div className="audio-control-pre">
            <img src={PreIcon} alt="" />
        </div>
        <div className="audio-control-play">
            <img src={Play} alt="" />
        </div>
        <div className="audio-control-next">
            <img src={NextIcon} alt="" />
        </div>
    </div>
复制代码

进度条的大体原理就是获取音频的当前播放时长以及音频总时长的比例,而后经过这个比例与进度条宽度相乘,能够获得当前播放时长下进度条须要被填充的宽度。bash

代码中的“audio-progress-bar-preload”是用来作缓冲条的,大概的作法也是同样,不过获取缓冲进度得用到audio的buffered属性,具体的用法推荐你们去MDN看看,在这里就很少赘述。框架

进度条以及播放按钮的布局代码大概就是这样,在css方面须要注意的就是进度条容器与进度条填充块以及进度条触点间的层级关系就好。ide

播放器功能逻辑

也许看了上面,你们以为很疑惑,为何没有见到最关键的audio标签。那是由于,其实整个播放器的逻辑重点都在那个标签里,咱们须要单独抽出来分析一下。oop

<audio preload="metadata" src={src} ref={(audio) => {this.lectureAudio = audio}} 
    style={{width: '1px', height: '1px', visibility: 'hidden'}} 
    onCanPlay={() => this.handleAudioCanplay()}
    onTimeUpdate={() => this.handleTimeUpdate()}>
</audio>
复制代码

① 怎么让进度条动起来?

解决思路--音频在播放的时候,当前进度(currentTime)是一直都在变化的,也就是说,能找到一个将currentTime变化具现为进度条长度的常数是关键。说的这么复杂,其实在上面的布局代码里已经剧透了,布局

calc(${currentTime}/${currentTotalTime} * $this.progressBar.clientWidth}px
复制代码

这样就能很轻松的算出当前进度须要对应的进度条填充长度。问题又来了,我知道进度条该填充多长了,可是它仍是不会动额... 在这里,咱们有两个方法能够解决:性能

  • 利用setInterval 咱们能够每隔300ms就检测一下当前audio的currentTime,而后在setState动态改变state中的currentTime,接着组件便会重渲染进度条部分的展现,从而也就让咱们的进度条动起来了。
  • 利用audio的ontimeupdate事件 这个是audio和video都拥有的原生事件,做用是--“当currentTime更新时会触发timeupdate事件”,通常推荐使用这种方式来动态计算进度条的宽度,毕竟能够少写一个计时器,说不定能够规避一些项目中的隐患。并且这是HTML的原生事件,浏览器的支持确定是充分的,因此从性能的角度来讲应该是比上一种方式要好。

ontimeupdate时执行的方法--每次触发该事件时都从新给currentTime赋值,剩下的改动均可以经过currentTime值的变化而作出相应的变化。this

handleTimeUpdate() {
    if (this.state.currentTime < (this.state.currentTotalTime - 1)) {
        this.setState({
            currentTime: this.lectureAudio.currentTime
        });
    } else {
        ......
    }
}
复制代码

② 怎么在移动端拖动进度条?

解决思路--既然是移动端的触碰事件,那么touch事件天然就是主角了。经过touch事件咱们能够计算出,拖动的距离,进而获得进度条以及触点该移动的距离。

initListenTouch() {
    this.audioPoint.addEventListener('touchstart', (e) => this.pointStart(e), false);
    this.audioPoint.addEventListener('touchmove', (e) => this.pointMove(e), false);
    this.audioPoint.addEventListener('touchend', (e) => this.pointEnd(e), false);
}
复制代码

这是组件加载时挂在到进度条触点的三个事件监听,这里讲一下三个监听具体都有些什么做用。

  • touchstart--负责获取触摸进度触点时触点的方位
pointStart(e) {
    e.preventDefault();
    let touch = e.touches[0];
    this.lectureAudio.pause();
    //为了更好的体验,在移动触点的时候我选择将音频暂停
    this.setState({
        isPlaying: false,//播放按钮变动
        startX: touch.pageX//进度触点在页面中的x坐标
    });
}
复制代码
  • touchmove--负责动态计算触点的拖动距离,并转换成this.state.currentTime从而触发组件的重渲染.
pointMove(e) {
    e.preventDefault();
    let touch = e.touches[0];
    let x = touch.pageX - this.state.startX; //滑动的距离
    let maxMove = this.progressBar.clientWidth;//最大的移动距离不可超过进度条宽度

    //(this.state.moveX) = this.lectureAudio.duration / this.progressBar.clientWidth;
    //moveX是一个固定的常数,它表明着进度条宽度与音频总时长的关系,咱们能够经过获取触点移动的距离从而计算出此时对应的currentTime
    //下面是触点移动时会碰到的状况,分为正移动、负移动以及两端的极限移动。
    if (x >= 0) {
        if (x + this.state.startX - this.offsetWindowLeft >= maxMove) {
            this.setState({
                currentTime: this.state.currentTotalTime,
                //改变当前播放时间的数值
            }, () => {
                this.lectureAudio.currentTime = this.state.currentTime;
                //改变audio真正的播放时间
            })
        } else {
            this.setState({
                currentTime: (x + this.state.startX - this.offsetWindowLeft) * this.state.moveX
            }, () => {
                this.lectureAudio.currentTime = this.state.currentTime;
            })
        }
    } else {
        if (-x <= this.state.startX - this.offsetWindowLeft) {
            this.setState({
                currentTime: (this.state.startX + x - this.offsetWindowLeft) * this.state.moveX,
            }, () => {
                this.lectureAudio.currentTime = this.state.currentTime;
            })
        } else {
            this.setState({
                currentTime: 0
            }, () => {
                this.lectureAudio.currentTime = this.state.currentTime;
            })
        }
    }
}
复制代码
  • touchend--负责恢复音频的播放
pointEnd(e) {
    e.preventDefault();
    if (this.state.currentTime < this.state.currentTotalTime) {
        this.touchendPlay = setTimeout(() => {
            this.handleAudioPlay();
        }, 300)
    }
    //关于300ms的setTimeout,一是为了体验的良好,你们在作的时候能够试试不要300ms的延迟,会发现收听体验很差,音频的播放十分仓促。
    //另外还有一点是,audio的pause与play间隔太短会出现报错,致使audio没法准确的执行相应的动做。
}
复制代码

③ 怎么实现列表播放与循环播放?

解决思路--时刻关注currentTime与duration之间的关系

handleTimeUpdate() {
        if (this.state.currentTime < (this.state.currentTotalTime - 1)){
            ......
        }else {
            //此状况为音频已播放至最后
            if (this.state.isLooping){//是否为循环播放
            //currentTime归零,而且手动将audio的currentTime设为0,并手动执行play()
                this.setState({
                    currentTime: 0
                }, () => {
                    this.lectureAudio.currentTime = this.state.currentTime;
                    this.lectureAudio.play();
                })
            }else {
                //列表播放则只需判断是否有下一首,有则跳转或播放,无则暂停播放。
                if (this.props.audioInfo.next_lecture_id && this.state.currentTime !== 0){
                    this.handleNextLecture();
                }else {
                    this.handleAudioPause();
                }
            }
        }
    }
复制代码

总结

不知道你们看完以后有没一种感受,好像不管是什么都离不开this.state中的currentTime。

是的,这个播放器的核心就是currentTime,这也是开发时的刻意为之,最后咱们会发现这个组件中的惟一变量就是currentTime,咱们能够经过currentTime的变化完成全部的需求,而且不须要考虑其余因素的影响,由于全部的子组件都是围绕着currentTime运转。

以上是我关于播放器开发的一点当心得,虽然没有十分酷炫的皮肤,也没有十分复杂的功能,可是对我的而言,它很好的帮我理清了一个[可用的]播放器的开发流程。

相关文章
相关标签/搜索