使用react native制作的一款网络音乐播放器 

基于第三方库 react-native-video 设计
"react-native-video": "^1.0.0" 

 播放/暂停

 快进/快退

 循环模式(单曲,随机,列表)

 歌词同步

 进度条显示

 播放时间

 基本旋转动画

 动画bug

 安卓歌词解析失败

 其他

使用的数据是百度音乐

 http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.billboard.billList&type=2&size=10&offset=0 //总列表
 http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.lry&songid=213508 //歌词文件
 http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.play&songid=877578 //播放

更多:http://67zixue.com/home/article/detail/id/22.html

主要代码
把秒数转换为时间类型:
    //把秒数转换为时间类型
    formatTime(time) {
        // 71s -> 01:11
        let min = Math.floor(time / 60)
        let second = time - min * 60
        min = min >= 10 ? min : '0' + min
        second = second >= 10 ? second : '0' + second
        return min + ':' + second
    } 

 歌词:
[ti:阳光总在风雨后] [ar:许美静] [al:都是夜归人] [00:05.97]阳光总在风雨后 [00:14.31]演唱:许美静......

 拿到当前歌曲的歌词后,如上,把这段字符截成一个这样的数组

 

 其算法如下:

 

let lry = responseJson.lrcContent
let lryAry = lry.split('\n')   //按照换行符切数组
lryAry.forEach(function (val, index) {
    var obj = {}   //用于存放时间
    val = val.replace(/(^\s*)|(\s*$)/g, '')    //正则,去除前后空格
    let indeofLastTime = val.indexOf(']')  // ]的下标
    let timeStr = val.substring(1, indeofLastTime) //把时间切出来 0:04.19
    let minSec = ''
    let timeMsIndex = timeStr.indexOf('.')  // .的下标
    if (timeMsIndex !== -1) {
        //存在毫秒 0:04.19
        minSec = timeStr.substring(1, val.indexOf('.'))  // 0:04.
        obj.ms = parseInt(timeStr.substring(timeMsIndex + 1, indeofLastTime))  //毫秒值 19
    } else {
        //不存在毫秒 0:04
        minSec = timeStr
        obj.ms = 0
    }
    let curTime = minSec.split(':')  // [0,04]
    obj.min = parseInt(curTime[0])   //分钟 0
    obj.sec = parseInt(curTime[1])   //秒钟 04
    obj.txt = val.substring(indeofLastTime + 1, val.length) //歌词文本: 留下唇印的嘴
    obj.txt = obj.txt.replace(/(^\s*)|(\s*$)/g, '')
    obj.dis = false
    obj.total = obj.min * 60 + obj.sec + obj.ms / 100   //总时间
    if (obj.txt.length > 0) {
        lyrObj.push(obj)
    }
})

  

歌词显示:

 // 歌词
    renderItem() {
        // 数组
        var itemAry = [];
        for (var i = 0; i < lyrObj.length; i++) {
            var item = lyrObj[i].txt
            if (this.state.currentTime.toFixed(2) > lyrObj[i].total) {
                //正在唱的歌词
                itemAry.push(
                    <View key={i} style={styles.itemStyle}>
                        <Text style={{ color: 'blue' }}> {item} </Text>
                    </View>
                );
                _scrollView.scrollTo({x: 0,y:(25 * i),animated:false});
            }
            else {
                //所有歌词
                itemAry.push(
                    <View key={i} style={styles.itemStyle}>
                        <Text style={{ color: 'red' }}> {item} </Text>
                    </View>
                )
            }
        }

        return itemAry;
    }
 

  

其余什么播放/暂停.时间显示,快进/快退,进度条都是根据react-native-video 而来.

完整代码:

 

/**
 * Created by shaotingzhou on 2017/4/13.
 */

import React, { Component } from 'react'
import {
    AppRegistry,
    StyleSheet,
    Dimensions,
    Text,
    Image,
    View,
    Slider,
    TouchableOpacity,
    ScrollView,
    ActivityIndicator,
    Animated,
    Easing
} from 'react-native'
var {width,height} = Dimensions.get('window');
import Video from 'react-native-video'
var lyrObj = []   // 存放歌词
var myAnimate;
//       http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.billboard.billList&type=2&size=10&offset=0    //总列表
//       http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.lry&songid=213508   //歌词文件
//       http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.play&songid=877578   //播放


export default class Main extends Component {

    constructor(props) {
        super(props);
        this.spinValue = new Animated.Value(0)
        this.state = {
            songs: [],   //歌曲id数据源
            playModel:1,  // 播放模式  1:列表循环    2:随机    3:单曲循环
            btnModel:require('./image/列表循环.png'), //播放模式按钮背景图
            pic_small:'',    //小图
            pic_big:'',      //大图
            file_duration:0,    //歌曲长度
            song_id:'',     //歌曲id
            title:'',       //歌曲名字
            author:'',      //歌曲作者
            file_link:'',   //歌曲播放链接
            songLyr:[],     //当前歌词
            sliderValue: 0,    //Slide的value
            pause:false,       //歌曲播放/暂停
            currentTime: 0.0,   //当前时间
            duration: 0.0,     //歌曲时间
            currentIndex:0,    //当前第几首
            isplayBtn:require('./image/播放.png')  //播放/暂停按钮背景图
        }
    }
    //上一曲
    prevAction = (index) =>{
        this.recover()
        lyrObj = [];
        if(index == -1){
            index = this.state.songs.length - 1 // 如果是第一首就回到最后一首歌
        }
        this.setState({
            currentIndex:index  //更新数据
        })
        this.loadSongInfo(index)  //加载数据
    }
    //下一曲
    nextAction = (index) =>{
        this.recover()
        lyrObj = [];
        if(index == 10){
            index = 0 //如果是最后一首就回到第一首
        }
        this.setState({
            currentIndex:index,  //更新数据
        })
        this.loadSongInfo(index)   //加载数据
    }
    //换歌时恢复进度条 和起始时间
    recover = () =>{
        this.setState({
            sliderValue:0,
            currentTime: 0.0
        })
    }
    //播放模式 接收传过来的当前播放模式 this.state.playModel
    playModel = (playModel) =>{
        playModel++;
        playModel = playModel == 4 ? 1 : playModel
        //重新设置
        this.setState({
            playModel:playModel
        })
        //根据设置后的模式重新设置背景图片
        if(playModel == 1){
            this.setState({
                btnModel:require('./image/列表循环.png'),
            })
        }else if(playModel ==  2){
            this.setState({
                btnModel:require('./image/随机.png'),
            })
        }else{
            this.setState({
                btnModel:require('./image/单曲循环.png'),
            })
        }
    }
    //播放/暂停
    playAction =() => {
        this.setState({
            pause: !this.state.pause
        })
        //判断按钮显示什么
        if(this.state.pause == true){
            this.setState({
                isplayBtn:require('./image/播放.png')
            })
        }else {
            this.setState({
                isplayBtn:require('./image/暂停.png')
            })
        }

    }
    //播放器每隔250ms调用一次
    onProgress =(data) => {
        let val = parseInt(data.currentTime)
        this.setState({
            sliderValue: val,
            currentTime: data.currentTime
        })

        //如果当前歌曲播放完毕,需要开始下一首
        if(val == this.state.file_duration){
            if(this.state.playModel == 1){
                //列表 就播放下一首
                this.nextAction(this.state.currentIndex + 1)
            }else if(this.state.playModel == 2){
                let  last =  this.state.songs.length //json 中共有几首歌
                let random = Math.floor(Math.random() * last)  //取 0~last之间的随机整数
                this.nextAction(random) //播放
            }else{
                //单曲 就再次播放当前这首歌曲
                this.refs.video.seek(0) //让video 重新播放
                _scrollView.scrollTo({x: 0,y:0,animated:false});
            }
        }

    }
    //把秒数转换为时间类型
    formatTime(time) {
        // 71s -> 01:11
        let min = Math.floor(time / 60)
        let second = time - min * 60
        min = min >= 10 ? min : '0' + min
        second = second >= 10 ? second : '0' + second
        return min + ':' + second
    }
    // 歌词
    renderItem() {
        // 数组
        var itemAry = [];
        for (var i = 0; i < lyrObj.length; i++) {
            var item = lyrObj[i].txt
            if (this.state.currentTime.toFixed(2) > lyrObj[i].total) {
                //正在唱的歌词
                itemAry.push(
                    <View key={i} style={styles.itemStyle}>
                        <Text style={{ color: 'blue' }}> {item} </Text>
                    </View>
                );
                _scrollView.scrollTo({x: 0,y:(25 * i),animated:false});
            }
            else {
                //所有歌词
                itemAry.push(
                    <View key={i} style={styles.itemStyle}>
                        <Text style={{ color: 'red' }}> {item} </Text>
                    </View>
                )
            }
        }

        return itemAry;
    }
    // 播放器加载好时调用,其中有一些信息带过来
    onLoad = (data) => {
        this.setState({ duration: data.duration });
    }

    loadSongInfo = (index) => {
        //加载歌曲
        let songid =  this.state.songs[index]
        let url = 'http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.play&songid=' + songid
        fetch(url)
            .then((response) => response.json())
            .then((responseJson) => {
                let songinfo = responseJson.songinfo
                let bitrate = responseJson.bitrate
                this.setState({
                    pic_small:songinfo.pic_small, //小图
                    pic_big:songinfo.pic_big,  //大图
                    title:songinfo.title,     //歌曲名
                    author:songinfo.author,   //歌手
                    file_link:bitrate.file_link,   //播放链接
                    file_duration:bitrate.file_duration //歌曲长度
                })

                //加载歌词
                let url = 'http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.song.lry&songid=' + songid
                fetch(url)
                    .then((response) => response.json())
                    .then((responseJson) => {

                        let lry = responseJson.lrcContent
                        let lryAry = lry.split('\n')   //按照换行符切数组
                        lryAry.forEach(function (val, index) {
                            var obj = {}   //用于存放时间
                            val = val.replace(/(^\s*)|(\s*$)/g, '')    //正则,去除前后空格
                            let indeofLastTime = val.indexOf(']')  // ]的下标
                            let timeStr = val.substring(1, indeofLastTime) //把时间切出来 0:04.19
                            let minSec = ''
                            let timeMsIndex = timeStr.indexOf('.')  // .的下标
                            if (timeMsIndex !== -1) {
                                //存在毫秒 0:04.19
                                minSec = timeStr.substring(1, val.indexOf('.'))  // 0:04.
                                obj.ms = parseInt(timeStr.substring(timeMsIndex + 1, indeofLastTime))  //毫秒值 19
                            } else {
                                //不存在毫秒 0:04
                                minSec = timeStr
                                obj.ms = 0
                            }
                            let curTime = minSec.split(':')  // [0,04]
                            obj.min = parseInt(curTime[0])   //分钟 0
                            obj.sec = parseInt(curTime[1])   //秒钟 04
                            obj.txt = val.substring(indeofLastTime + 1, val.length) //歌词文本: 留下唇印的嘴
                            obj.txt = obj.txt.replace(/(^\s*)|(\s*$)/g, '')
                            obj.dis = false
                            obj.total = obj.min * 60 + obj.sec + obj.ms / 100   //总时间
                            if (obj.txt.length > 0) {
                                lyrObj.push(obj)
                            }
                        })
                    })

            })
    }


    componentWillMount() {
        //先从总列表中获取到song_id保存
        fetch('http://tingapi.ting.baidu.com/v1/restserver/ting?method=baidu.ting.billboard.billList&type=2&size=10&offset=0')
            .then((response) => response.json())
            .then((responseJson) => {
                  var listAry = responseJson.song_list
                var song_idAry = []; //保存song_id的数组
                for(var i = 0;i<listAry.length;i++){
                      let song_id = listAry[i].song_id
                      song_idAry.push(song_id)
                  }
                this.setState({
                    songs:song_idAry
                })
                this.loadSongInfo(0)   //预先加载第一首
            })

        this.spin()   //   启动旋转

    }

    //旋转动画
    spin () {
        this.spinValue.setValue(0)
        myAnimate = Animated.timing(
            this.spinValue,
            {
                toValue: 1,
                duration: 4000,
                easing: Easing.linear
            }
        ).start(() => this.spin())

    }

    render() {
        //如果未加载出来数据 就一直转菊花
        if (this.state.file_link.length <= 0 ) {
            return(
                <ActivityIndicator
                    animating={this.state.animating}
                    style={{flex: 1,alignItems: 'center',justifyContent: 'center'}}
                    size="large" />
            )
        }else{
            const spin = this.spinValue.interpolate({
                inputRange: [0, 1],
                outputRange: ['0deg', '360deg']
            })


            //数据加载出来
            return (
                <View style={styles.container}>
                    {/*背景大图*/}
                    <Image source={{uri:this.state.pic_big}} style={{flex:1}}/>
                    {/*背景白色透明遮罩*/}
                    <View style = {{position:'absolute',width: width,height:height,backgroundColor:'white',opacity:0.8}}/>

                    <View style = {{position:'absolute',width: width}}>
                        {/*胶片光盘*/}
                        <Image source={require('./image/胶片盘.png')} style={{width:220,height:220,alignSelf:'center'}}/>

                        {/*旋转小图*/}
                        <Animated.Image
                            ref = 'myAnimate'
                            style={{width:140,height:140,marginTop: -180,alignSelf:'center',borderRadius: 140*0.5,transform: [{rotate: spin}]}}
                            source={{uri: this.state.pic_small}}
                        />

                        {/*播放器*/}
                        <Video
                            source={{uri: this.state.file_link}}
                            ref='video'
                            volume={1.0}
                            paused={this.state.pause}
                            onProgress={(e) => this.onProgress(e)}
                            onLoad={(e) => this.onLoad(e)}
                        />
                        {/*歌曲信息*/}
                        <View style={styles.playingInfo}>
                            {/*作者-歌名*/}
                            <Text>{this.state.author} - {this.state.title}</Text>
                            {/*时间*/}
                            <Text>{this.formatTime(Math.floor(this.state.currentTime))} - {this.formatTime(Math.floor(this.state.duration))}</Text>
                        </View>
                        {/*播放模式*/}
                        <View style = {{marginTop: 5,marginBottom:5,marginLeft: 20}}>
                            <TouchableOpacity onPress={()=>this.playModel(this.state.playModel)}>
                                <Image source={this.state.btnModel} style={{width:20,height:20}}/>
                            </TouchableOpacity>
                        </View>
                        {/*进度条*/}
                        <Slider
                            ref='slider'
                            style={{ marginLeft: 10, marginRight: 10}}
                            value={this.state.sliderValue}
                            maximumValue={this.state.file_duration}
                            step={1}
                            minimumTrackTintColor='#FFDB42'
                            onValueChange={(value) => {
                              this.setState({
                                  currentTime:value
                              })
							            }
						            }
                            onSlidingComplete={(value) => {
							             this.refs.video.seek(value)
							        }}
                        />
                        {/*歌曲按钮*/}
                        <View style = {{flexDirection:'row',justifyContent:'space-around'}}>
                            <TouchableOpacity onPress={()=>this.prevAction(this.state.currentIndex - 1)}>
                                <Image source={require('./image/上一首.png')} style={{width:30,height:30}}/>
                            </TouchableOpacity>

                            <TouchableOpacity onPress={()=>this.playAction()}>
                                <Image source={this.state.isplayBtn} style={{width:30,height:30}}/>
                            </TouchableOpacity>

                            <TouchableOpacity onPress={()=>this.nextAction(this.state.currentIndex + 1)}>
                                <Image source={require('./image/下一首.png')} style={{width:30,height:30}}/>
                            </TouchableOpacity>
                        </View>

                        {/*歌词*/}
                        <View style={{height:140,alignItems:'center'}}>

                            <ScrollView style={{position:'relative'}}
                                        ref={(scrollView) => { _scrollView = scrollView}}
                            >
                                {this.renderItem()}
                            </ScrollView>
                        </View>
                    </View>

                </View>
            )
        }

    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    image: {
        flex: 1
    },
    playingControl: {
        flexDirection: 'row',
        alignItems: 'center',
        paddingTop: 10,
        paddingLeft: 20,
        paddingRight: 20,
        paddingBottom: 20
    },
    playingInfo: {
        flexDirection: 'row',
        alignItems:'stretch',
        justifyContent: 'space-between',
        paddingTop: 40,
        paddingLeft: 20,
        paddingRight: 20,
        backgroundColor:'rgba(255,255,255,0.0)'
    },
    text: {
        color: "black",
        fontSize: 22
    },
    modal: {
        height: 300,
        borderTopLeftRadius: 5,
        borderTopRightRadius: 5,
        paddingTop: 5,
        paddingBottom: 50
    },
    itemStyle: {
        paddingTop: 20,
        height:25,
        backgroundColor:'rgba(255,255,255,0.0)'
    }
})

  

 

 

github地址: https://github.com/pheromone/react-native-videoDemo