使用react native製做的一款網絡音樂播放器 javascript
基於第三方庫 react-native-video 設計
"react-native-video": "^1.0.0" html
播放/暫停java
快進/快退react
循環模式(單曲,隨機,列表)git
歌詞同步github
進度條顯示算法
播放時間json
基本旋轉動畫react-native
動畫bugapi
安卓歌詞解析失敗
其餘
使用的數據是百度音樂
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