使用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
