移动端使用原生audio标签制作react 音频组件

需求

要实现音频的播放如下图:

html

html代码如下:

<audio src="" preload="metadata" controls />

本来我以为在css3这么强大的年代,自定义一个audio的皮肤应该是完全没问题的,后来的事实证明too young too simple。

看了下audio的shadow dom结构,然后试了试用css去自定义,于是发现两个问题:

  • 第一个为播放暂停按钮,就是一个标签没有状态,默认的css定义是为-webkit-appearance: media-play-button;,一个样式控制两种状态,没招。
  • 第二个为中间的进度条,自身是个shadow dom,于是构成了两层shadow dom(audio本身是一层),这也没招。

于是只好转向js来控制了,html修改如下:

<div class="audio-wrap"> 
    <audio  src="" preload="metadata" controls />
    <i  class="icon-play"></i> <!-- 播放/暂停按钮 通过js切换class -->
    <div clas="timeline"> <!-- 进度条 -->
        <div class="playhead"></div>
    </div>
    <div class="time-num">  <!-- 时间 -->
        <span class="num-current">00:00</span> / <span class="num-duration">00:00</span>
    </div>
</div>

事件

  • audio的loadedmetadata事件,读取音频的总时长
  • audio的timeupdate事件,用于更新播放进度
  • audio的canplaythrough事件,是否能够不停下来进行缓冲的情况下持续播放指定的音频/视频
  • icon-play的点击事件,暂停或播放
  • timeline的点击事件,用于跳跃播放

react 组件

目前采用的es5,audio地址通过props传入,判断播放还是暂停采用state切换,进度条更新用了reactDOM操作。

var React = require('react');
var ReactDOM = React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

// 简单格式化时间,小于9的数字前面添加0
function formatTime(num) {
    var num = parseInt(num);
    if(num <= 60) {
        if(num < 10) {
            num = '0' + num;
        }
        return num;
    }
}

module.exports = React.createClass({
    getInitialState: function() {
        return {
            isPlay: false // 默认暂停
        }
    },
    componentDidMount: function() {
        var audioNode = ReactDOM.findDOMNode(this.refs.audio),
            playNode = ReactDOM.findDOMNode(this.refs.play),
            timeline = ReactDOM.findDOMNode(this.refs.timeline),
            playhead = ReactDOM.findDOMNode(this.refs.playhead),
            timeCurrent = ReactDOM.findDOMNode(this.refs.timeCurrent),
            timeDuration = ReactDOM.findDOMNode(this.refs.timeDuration),
            timelineWidth = timeline.offsetWidth - playhead.offsetWidth,
            that = this,
            duration;

        // 得到初始数据
        function loadedmetadata() {
            timeDuration = '00:'+ formatTime(audioNode.duration);
            timeCurrent = '00:00';
        }
        this.loadedmetadata = loadedmetadata;

        // 播放进度
        function timeUpdate() {
            var playPercent = timelineWidth * (audioNode.currentTime / duration);
            playhead.style.webkitTransform  = "translateX("+playPercent + "px)";
            playhead.style.transform = "translateX("+playPercent + "px)";
            if (audioNode.currentTime == duration) {
                that.setState({
                    isPlay: false
                })
            }
            timeCurrent = '00:'+ formatTime(audioNode.currentTime);
        }
        this.timeUpdate = timeUpdate;

        // 是否能够不停下来进行缓冲的情况下持续播放指定的音频/视频
        function canplaythrough() {
            duration = audioNode.duration;
        }
        this.canplaythrough = canplaythrough;

        // 进度条点击
        function timelineClick(e) {
            // 更新坐标位置
            var newLeft = e.pageX - timeline.offsetLeft;
            if (newLeft >= 0 && newLeft <= timelineWidth) {
                playhead.style.transform = "translateX("+ newLeft +"px)";
            }
            if (newLeft < 0) {
                playhead.style.transform = "translateX(0)";
            }
            if (newLeft > timelineWidth) {
                playhead.style.transform = "translateX("+ timelineWidth + "px)";
            }
            // 更新时间
            audioNode.currentTime = duration * (e.pageX - timeline.offsetLeft) / timelineWidth;
        }
        this.timelineClick = timelineClick;

        // 监听事件
        audioNode.addEventListener("loadedmetadata", that.loadedmetadata);
        audioNode.addEventListener("timeupdate", that.timeUpdate);
        audioNode.addEventListener("canplaythrough", that.canplaythrough);
        timeline.addEventListener("click", that.timelineClick);
    },
    componentWillUnmount: function() {
        var audioNode = ReactDOM.findDOMNode(this.refs.audio),
            timeline = ReactDOM.findDOMNode(this.refs.timeline);

        // 注销事件
        audioNode.removeEventListener("loadedmetadata", this.loadedmetadata);
        audioNode.removeEventListener("timeupdate", this.timeUpdate);
        audioNode.removeEventListener("canplaythrough", this.canplaythrough);
        timeline.removeEventListener("click", this.timelineClick);
    },
    play: function(){
        var audioNode = ReactDOM.findDOMNode(this.refs.audio);

        this.setState({
            isPlay: !this.state.isPlay
        })

        if (!this.state.isPlay) {
            audioNode.play();
        } else { // pause music
            audioNode.pause();
        }
    },
    render: function() {
        return (
            <div className="audio-wrap"> 
                <audio ref="audio" src={this.props.audioUrl} preload="metadata" controls />
                <i ref="play" className={"icon-play" + (this.state.isPlay ? " pause" : "")} onClick={this.play}></i>
                <div ref="timeline" className="timeline">
                      <div ref="playhead" className="playhead"></div>
                </div>
                <div className="time-num">
                    <span ref="timeCurrent" className="num-current">00:00</span> / <span ref="timeDuration" className="num-duration">00:00</span>
                </div>
            </div>
        )
    },
    propTypes: {
        audioUrl: React.PropTypes.string.isRequired
    }
})

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

扫码关注云+社区